From 9a2bf4980a3d5144365d0dde06935758960cd99e Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 14:37:32 +0100 Subject: [PATCH 01/11] add prettier config --- package.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package.json b/package.json index 7cd7862..5add654 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,22 @@ "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", "test": "jest" }, + "prettier": { + "printWidth": 120, + "tabWidth": 2, + "quoteProps": "consistent", + "trailingComma": "all", + "overrides": [ + { + "files": [ + "src/**/*.ts" + ], + "options": { + "tabWidth": 4 + } + } + ] + }, "files": [ "src", "lib", From b5415744347452e42f4d326ae4c3f7fca5bfe986 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:14:52 +0100 Subject: [PATCH 02/11] Prettier in test files --- package.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5add654..30c250e 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,20 @@ "overrides": [ { "files": [ - "src/**/*.ts" + "src/**/*.ts", + "test/**/*.ts" ], "options": { "tabWidth": 4 } + }, + { + "files": [ + "test/**/*.ts" + ], + "options": { + "singleQuote": true + } } ] }, From b9737bf9382119a7de4da6fe3ebbf15a2fa996b1 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:17:58 +0100 Subject: [PATCH 03/11] apply prettier --- .babelrc | 11 +- .eslintrc.js | 92 +-- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/sonarqube.yml | 24 +- README.md | 20 +- examples/widget/index.css | 25 +- examples/widget/index.html | 62 +- examples/widget/utils.js | 14 +- jest.config.ts | 12 +- package.json | 1 + renovate.json | 4 +- src/ClientWidgetApi.ts | 384 +++++++------ src/WidgetApi.ts | 223 ++++---- src/driver/WidgetDriver.ts | 33 +- src/interfaces/Capabilities.ts | 20 +- src/interfaces/DownloadFileAction.ts | 22 +- src/interfaces/GetMediaConfigAction.ts | 20 +- src/interfaces/IWidgetApiErrorResponse.ts | 9 +- src/interfaces/LanguageChangeAction.ts | 8 +- src/interfaces/ReadRelationsAction.ts | 2 +- src/interfaces/SendEventAction.ts | 3 +- src/interfaces/StickerAction.ts | 3 +- src/interfaces/ThemeChangeAction.ts | 2 +- src/interfaces/TurnServerActions.ts | 3 +- src/interfaces/UploadFileAction.ts | 22 +- src/models/WidgetEventCapability.ts | 24 +- src/models/WidgetParser.ts | 14 +- src/templating/url-template.ts | 24 +- src/transport/ITransport.ts | 7 +- src/transport/PostmessageTransport.ts | 19 +- test/ClientWidgetApi-test.ts | 512 +++++++---------- test/WidgetApi-test.ts | 655 ++++++++++------------ test/url-template-test.ts | 22 +- tsconfig-dev.json | 4 +- tsconfig.json | 9 +- yarn.lock | 5 + 37 files changed, 1070 insertions(+), 1248 deletions(-) diff --git a/.babelrc b/.babelrc index 028dc10..e0f4da4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,5 @@ { - "sourceMaps": true, - "presets": [ - "@babel/preset-env", - "@babel/preset-typescript" - ], - "plugins": [ - "@babel/plugin-proposal-class-properties" - ] + "sourceMaps": true, + "presets": ["@babel/preset-env", "@babel/preset-typescript"], + "plugins": ["@babel/plugin-proposal-class-properties"] } diff --git a/.eslintrc.js b/.eslintrc.js index 2ce44f3..02a10fe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,50 +1,52 @@ module.exports = { - plugins: [ - "matrix-org", + plugins: ["matrix-org"], + extends: ["plugin:matrix-org/babel"], + parserOptions: { + project: ["./tsconfig-dev.json"], + }, + env: { + browser: true, + }, + rules: { + "no-var": ["warn"], + "prefer-rest-params": ["warn"], + "prefer-spread": ["warn"], + "one-var": ["warn"], + "padded-blocks": ["warn"], + "no-extend-native": ["warn"], + "camelcase": ["warn"], + "no-multi-spaces": ["error", { ignoreEOLComments: true }], + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always", + }, ], - extends: [ - "plugin:matrix-org/babel", - ], - parserOptions: { - project: ["./tsconfig-dev.json"], - }, - env: { - browser: true, - }, - rules: { - "no-var": ["warn"], - "prefer-rest-params": ["warn"], - "prefer-spread": ["warn"], - "one-var": ["warn"], - "padded-blocks": ["warn"], - "no-extend-native": ["warn"], - "camelcase": ["warn"], - "no-multi-spaces": ["error", { "ignoreEOLComments": true }], - "space-before-function-paren": ["error", { - "anonymous": "never", - "named": "never", - "asyncArrow": "always", - }], - "arrow-parens": "off", - "prefer-promise-reject-errors": "off", + "arrow-parens": "off", + "prefer-promise-reject-errors": "off", + "quotes": "off", + "indent": "off", + "no-constant-condition": "off", + "no-async-promise-executor": "off", + }, + overrides: [ + { + files: ["src/**/*.ts", "test/**/*.ts"], + extends: ["plugin:matrix-org/typescript"], + rules: { + // TypeScript has its own version of this + "babel/no-invalid-this": "off", + "quotes": "off", - "indent": "off", - "no-constant-condition": "off", - "no-async-promise-executor": "off", + }, }, - overrides: [{ - "files": ["src/**/*.ts", "test/**/*.ts"], - "extends": ["plugin:matrix-org/typescript"], - "rules": { - // TypeScript has its own version of this - "babel/no-invalid-this": "off", - - "quotes": "off", - }, - }, { - "files": ["src/interfaces/**/*.ts"], - "rules": { - "@typescript-eslint/no-empty-object-type": "off", - }, - }], + { + files: ["src/interfaces/**/*.ts"], + rules: { + "@typescript-eslint/no-empty-object-type": "off", + }, + }, + ], }; diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d9a3e29..77c1982 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - cache: 'yarn' + cache: "yarn" - name: Install NPM packages run: yarn install --frozen-lockfile diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fbc5277..f862c2c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,7 +27,7 @@ jobs: - name: 🔧 Set up node environment uses: actions/setup-node@v4 with: - cache: 'yarn' + cache: "yarn" - name: 🛠️ Setup run: yarn install --pure-lockfile diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index c539966..2773eaa 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,16 +1,16 @@ name: SonarQube on: - workflow_run: - workflows: ["Build and test"] - types: - - completed + workflow_run: + workflows: ["Build and test"] + types: + - completed concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true jobs: - sonarqube: - name: 🩻 SonarQube - uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + sonarqube: + name: 🩻 SonarQube + uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/README.md b/README.md index 56bef5f..6016876 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ JavaScript/TypeScript SDK for widgets & clients to communicate. For help and support, visit [#matrix-widgets:matrix.org](https://matrix.to/#/#matrix-widgets:matrix.org) on Matrix. -*Disclaimer: Widgets are not yet in the Matrix spec, so this library may not work with other implementations.* +_Disclaimer: Widgets are not yet in the Matrix spec, so this library may not work with other implementations._ ## Building @@ -45,14 +45,14 @@ api.requestCapabilities(StickerpickerCapabilities); // Add custom action handlers (if needed) api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, {}); + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, {}); }); api.on("action:com.example.my_action", (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, {custom: "reply"}); + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, { custom: "reply" }); }); // Start the messaging @@ -63,7 +63,7 @@ api.sendContentLoaded(); // Later, do something else (if needed) api.setAlwaysOnScreen(true); -api.transport.send("com.example.my_action", {isExample: true}); +api.transport.send("com.example.my_action", { isExample: true }); ``` For a more complete example, see the `examples` directory of this repo. @@ -82,8 +82,8 @@ const api = new ClientWidgetApi(widget, iframe, driver); // The API is automatically started, so we just have to wait for a ready before doing something api.on("ready", () => { - api.updateVisibility(true).then(() => console.log("Widget knows it is visible now")); - api.transport.send("com.example.my_action", {isExample: true}); + api.updateVisibility(true).then(() => console.log("Widget knows it is visible now")); + api.transport.send("com.example.my_action", { isExample: true }); }); // Eventually, stop the API handling diff --git a/examples/widget/index.css b/examples/widget/index.css index b20b561..f8c9db1 100644 --- a/examples/widget/index.css +++ b/examples/widget/index.css @@ -14,25 +14,26 @@ * limitations under the License. */ -html, body { - background-color: #ffffff; - color: #000000; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +html, +body { + background-color: #ffffff; + color: #000000; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } body { - padding: 20px; + padding: 20px; } button { - border: none; - color: #ffffff; - background-color: #2a9d8f; - border-radius: 4px; - padding: 6px 12px; - cursor: pointer; + border: none; + color: #ffffff; + background-color: #2a9d8f; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; } #stickyState { - color: #3d5a80; + color: #3d5a80; } diff --git a/examples/widget/index.html b/examples/widget/index.html index 6e1c682..9b9ceef 100644 --- a/examples/widget/index.html +++ b/examples/widget/index.html @@ -13,9 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + Example Widget - - + +
Loading...
@@ -55,55 +55,59 @@ - + diff --git a/examples/widget/utils.js b/examples/widget/utils.js index 2d8076b..0a3cd49 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,17 +15,17 @@ */ function parseFragment() { - const fragmentString = (window.location.hash || "?"); - return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf('?'), 0))); + const fragmentString = window.location.hash || "?"; + return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); } function assertParam(fragment, name) { - const val = fragment.get(name); - if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); - return val; + const val = fragment.get(name); + if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); + return val; } function handleError(e) { - console.error(e); - document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; + console.error(e); + document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; } diff --git a/jest.config.ts b/jest.config.ts index 6c6af37..99641ec 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -19,16 +19,16 @@ import { env } from "process"; import type { Config } from "jest"; const config: Config = { - testEnvironment: "jsdom", - testMatch: ["/test/**/*-test.[jt]s?(x)"], - collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], - coverageReporters: ["text-summary", "lcov"], - testResultsProcessor: "@casualbot/jest-sonar-reporter", + testEnvironment: "jsdom", + testMatch: ["/test/**/*-test.[jt]s?(x)"], + collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], + coverageReporters: ["text-summary", "lcov"], + testResultsProcessor: "@casualbot/jest-sonar-reporter", }; // if we're running under GHA, enable the GHA reporter if (env["GITHUB_ACTIONS"] !== undefined) { - config.reporters = [["github-actions", { silent: false }], "summary"]; + config.reporters = [["github-actions", { silent: false }], "summary"]; } export default config; diff --git a/package.json b/package.json index 30c250e..4851ef9 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "eslint-plugin-unicorn": "^56.0.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "prettier": "3.4.2", "rimraf": "^3.0.2", "tinyify": "^3.0.0", "ts-node": "^10.9.1", diff --git a/renovate.json b/renovate.json index 5db72dd..22a9943 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] + "extends": ["config:recommended"] } diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 1fd0cd0..d3bf49d 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -174,12 +174,7 @@ export class ClientWidgetApi extends EventEmitter { if (!driver) { throw new Error("Invalid driver"); } - this.transport = new PostmessageTransport( - WidgetApiDirection.ToWidget, - widget.id, - iframe.contentWindow, - window, - ); + this.transport = new PostmessageTransport(WidgetApiDirection.ToWidget, widget.id, iframe.contentWindow, window); this.transport.targetOrigin = widget.origin; this.transport.on("message", this.handleMessage.bind(this)); @@ -193,36 +188,38 @@ export class ClientWidgetApi extends EventEmitter { } public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean { - return this.hasCapability(`org.matrix.msc2762.timeline:${Symbols.AnyRoom}`) - || this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`); + 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)); + 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)); + 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)); + 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)); + 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)); + 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)); + return this.allowedEvents.some((e) => e.matchesAsToDeviceEvent(EventDirection.Receive, eventType)); } public canReceiveRoomAccountData(eventType: string): boolean { - return this.allowedEvents.some(e => e.matchesAsRoomAccountData(EventDirection.Receive, eventType)); + return this.allowedEvents.some((e) => e.matchesAsRoomAccountData(EventDirection.Receive, eventType)); } public stop(): void { @@ -235,17 +232,19 @@ export class ClientWidgetApi extends EventEmitter { 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 => { - this.allowCapabilities([...allowedCaps], requestedCaps); - this.emit("ready"); - }).catch(e => { - this.emit("error:preparing", e); - }); + this.transport + .send(WidgetApiToWidgetAction.Capabilities, {}) + .then((caps) => { + requestedCaps = caps.capabilities; + return this.driver.validateCapabilities(new Set(caps.capabilities)); + }) + .then((allowedCaps) => { + this.allowCapabilities([...allowedCaps], requestedCaps); + this.emit("ready"); + }) + .catch((e) => { + this.emit("error:preparing", e); + }); } private allowCapabilities(allowed: string[], requested: string[]): void { @@ -255,14 +254,17 @@ export class ClientWidgetApi extends EventEmitter { const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); this.allowedEvents.push(...allowedEvents); - this.transport.send(WidgetApiToWidgetAction.NotifyCapabilities, { - requested, - approved: Array.from(this.allowedCapabilities), - }).catch(e => { - console.warn("non-fatal error notifying widget of approved capabilities:", e); - }).then(() => { - this.emit("capabilitiesNotified"); - }); + this.transport + .send(WidgetApiToWidgetAction.NotifyCapabilities, { + requested, + approved: Array.from(this.allowedCapabilities), + }) + .catch((e) => { + console.warn("non-fatal error notifying widget of approved capabilities:", e); + }) + .then(() => { + this.emit("capabilitiesNotified"); + }); // Push the initial room state for all rooms with a timeline capability for (const c of allowed) { @@ -307,14 +309,17 @@ export class ClientWidgetApi extends EventEmitter { this.contentLoadedWaitTimer = undefined; } if (this.contentLoadedActionSent) { - throw new Error("Improper sequence: ContentLoaded Action can only be sent once after the widget loaded " - +"and should only be used if waitForIframeLoad is false (default=true)"); + throw new Error( + "Improper sequence: ContentLoaded Action can only be sent 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 " - +"waitForIframeLoad is true (default=true)", + message: + "Improper sequence: not expecting ContentLoaded event if " + + "waitForIframeLoad is true (default=true)", }, }); } else { @@ -335,26 +340,27 @@ export class ClientWidgetApi extends EventEmitter { this.transport.reply(request, {}); const requested = request.data?.capabilities || []; - const newlyRequested = new Set(requested.filter(r => !this.hasCapability(r))); + const newlyRequested = new Set(requested.filter((r) => !this.hasCapability(r))); if (newlyRequested.size === 0) { // Nothing to do - skip validation this.allowCapabilities([], []); } - this.driver.validateCapabilities(newlyRequested) - .then(allowed => this.allowCapabilities([...allowed], [...newlyRequested])); + this.driver + .validateCapabilities(newlyRequested) + .then((allowed) => this.allowCapabilities([...allowed], [...newlyRequested])); } private handleNavigate(request: INavigateActionRequest): void { if (!this.hasCapability(MatrixCapabilities.MSC2931Navigate)) { return this.transport.reply(request, { - error: {message: "Missing capability"}, + 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"}, + error: { message: "Invalid matrix.to URI" }, }); } @@ -364,9 +370,12 @@ export class ClientWidgetApi extends EventEmitter { }; try { - this.driver.navigate(request.data.uri.toString()).catch((e: unknown) => onErr(e)).then(() => { - return this.transport.reply(request, {}); - }); + this.driver + .navigate(request.data.uri.toString()) + .catch((e: unknown) => onErr(e)) + .then(() => { + return this.transport.reply(request, {}); + }); } catch (e) { return onErr(e); } @@ -375,7 +384,10 @@ export class ClientWidgetApi extends EventEmitter { private handleOIDC(request: IGetOpenIDActionRequest): void { let phase = 1; // 1 = initial request, 2 = after user manual confirmation - const replyState = (state: OpenIDRequestState, credential?: IOpenIDCredentials): void | Promise => { + const replyState = ( + state: OpenIDRequestState, + credential?: IOpenIDCredentials, + ): void | Promise => { credential = credential || {}; if (phase > 1) { return this.transport.send( @@ -402,12 +414,12 @@ export class ClientWidgetApi extends EventEmitter { return replyState(OpenIDRequestState.Blocked); } else { return this.transport.reply(request, { - error: {message: msg}, + error: { message: msg }, }); } }; - const observer = new SimpleObservable(update => { + 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"); @@ -438,24 +450,24 @@ export class ClientWidgetApi extends EventEmitter { if (!this.canReceiveRoomAccountData(request.data.type)) { return this.transport.reply(request, { - error: {message: "Cannot read room account data of this type"}, + error: { message: "Cannot read room account data of this type" }, }); } return events.then((evs) => { - this.transport.reply(request, {events: evs}); + this.transport.reply(request, { events: evs }); }); } private async handleReadEvents(request: IReadEventFromWidgetActionRequest): Promise { if (!request.data.type) { return this.transport.reply(request, { - error: {message: "Invalid request - missing event type"}, + 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"}, + error: { message: "Invalid request - limit out of range" }, }); } @@ -463,13 +475,13 @@ export class ClientWidgetApi extends EventEmitter { if (request.data.room_ids === undefined) { askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId]; } else if (request.data.room_ids === Symbols.AnyRoom) { - askRoomIds = this.driver.getKnownRooms().filter(roomId => this.canUseRoomTimeline(roomId)); + askRoomIds = this.driver.getKnownRooms().filter((roomId) => this.canUseRoomTimeline(roomId)); } else { askRoomIds = request.data.room_ids; for (const roomId of askRoomIds) { if (!this.canUseRoomTimeline(roomId)) { return this.transport.reply(request, { - error: {message: `Unable to access room timeline: ${roomId}`}, + error: { message: `Unable to access room timeline: ${roomId}` }, }); } } @@ -484,14 +496,14 @@ export class ClientWidgetApi extends EventEmitter { 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"}, + error: { message: "Cannot read state events of this type" }, }); } } else { msgtype = request.data.msgtype; if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { return this.transport.reply(request, { - error: {message: "Cannot read room events of this type"}, + error: { message: "Cannot read room events of this type" }, }); } } @@ -499,45 +511,46 @@ export class ClientWidgetApi extends EventEmitter { // For backwards compatibility we still call the deprecated // readRoomEvents and readStateEvents methods in case the client isn't // letting us know the currently viewed room via setViewedRoomId - const events = request.data.room_ids === undefined && askRoomIds.length === 0 - ? await ( - request.data.state_key === undefined - ? this.driver.readRoomEvents(request.data.type, msgtype, limit, null, since) - : this.driver.readStateEvents(request.data.type, stateKey, limit, null) - ) - : ( - await Promise.all(askRoomIds.map(roomId => - this.driver.readRoomTimeline(roomId, request.data.type, msgtype, stateKey, limit, since), - )) - ).flat(1); + const events = + request.data.room_ids === undefined && askRoomIds.length === 0 + ? await (request.data.state_key === undefined + ? this.driver.readRoomEvents(request.data.type, msgtype, limit, null, since) + : this.driver.readStateEvents(request.data.type, stateKey, limit, null)) + : ( + await Promise.all( + askRoomIds.map((roomId) => + this.driver.readRoomTimeline(roomId, request.data.type, msgtype, stateKey, limit, since), + ), + ) + ).flat(1); this.transport.reply(request, { events }); } private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { if (!request.data.type) { return this.transport.reply(request, { - error: {message: "Invalid request - missing event type"}, + 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}`}, + error: { message: `Unable to access room timeline: ${request.data.room_id}` }, }); } const isDelayedEvent = request.data.delay !== undefined || request.data.parent_delay_id !== undefined; if (isDelayedEvent && !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent)) { return this.transport.reply(request, { - error: {message: "Missing capability"}, + error: { message: "Missing capability" }, }); } - let sendEventPromise: Promise; + let sendEventPromise: Promise; if (request.data.state_key !== undefined) { if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { return this.transport.reply(request, { - error: {message: "Cannot send state events of this type"}, + error: { message: "Cannot send state events of this type" }, }); } @@ -559,11 +572,11 @@ export class ClientWidgetApi extends EventEmitter { ); } } else { - const content = request.data.content as { msgtype?: string } || {}; - const msgtype = content['msgtype']; + 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"}, + error: { message: "Cannot send room events of this type" }, }); } @@ -586,31 +599,35 @@ export class ClientWidgetApi extends EventEmitter { } } - sendEventPromise.then(sentEvent => { - return this.transport.reply(request, { - room_id: sentEvent.roomId, - ...("eventId" in sentEvent ? { - event_id: sentEvent.eventId, - } : { - delay_id: sentEvent.delayId, - }), + sendEventPromise + .then((sentEvent) => { + return this.transport.reply(request, { + room_id: sentEvent.roomId, + ...("eventId" in sentEvent + ? { + event_id: sentEvent.eventId, + } + : { + delay_id: sentEvent.delayId, + }), + }); + }) + .catch((e: unknown) => { + console.error("error sending event: ", e); + this.handleDriverError(e, request, "Error sending event"); }); - }).catch((e: unknown) => { - console.error("error sending event: ", e); - this.handleDriverError(e, request, "Error sending event"); - }); } private handleUpdateDelayedEvent(request: IUpdateDelayedEventFromWidgetActionRequest): void { if (!request.data.delay_id) { return this.transport.reply(request, { - error: {message: "Invalid request - missing delay_id"}, + error: { message: "Invalid request - missing delay_id" }, }); } if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { return this.transport.reply(request, { - error: {message: "Missing capability"}, + error: { message: "Missing capability" }, }); } @@ -618,16 +635,19 @@ export class ClientWidgetApi extends EventEmitter { case UpdateDelayedEventAction.Cancel: case UpdateDelayedEventAction.Restart: case UpdateDelayedEventAction.Send: - this.driver.updateDelayedEvent(request.data.delay_id, request.data.action).then(() => { - return this.transport.reply(request, {}); - }).catch((e: unknown) => { - console.error("error updating delayed event: ", e); - this.handleDriverError(e, request, "Error updating delayed event"); - }); + this.driver + .updateDelayedEvent(request.data.delay_id, request.data.action) + .then(() => { + return this.transport.reply(request, {}); + }) + .catch((e: unknown) => { + console.error("error updating delayed event: ", e); + this.handleDriverError(e, request, "Error updating delayed event"); + }); break; default: return this.transport.reply(request, { - error: {message: "Invalid request - unsupported action"}, + error: { message: "Invalid request - unsupported action" }, }); } } @@ -635,19 +655,19 @@ export class ClientWidgetApi extends EventEmitter { private async handleSendToDevice(request: ISendToDeviceFromWidgetActionRequest): Promise { if (!request.data.type) { await this.transport.reply(request, { - error: {message: "Invalid request - missing event type"}, + error: { message: "Invalid request - missing event type" }, }); } else if (!request.data.messages) { await this.transport.reply(request, { - error: {message: "Invalid request - missing event contents"}, + 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"}, + 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"}, + error: { message: "Cannot send to-device events of this type" }, }); } else { try { @@ -682,7 +702,7 @@ export class ClientWidgetApi extends EventEmitter { private async handleWatchTurnServers(request: IWatchTurnServersRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { await this.transport.reply(request, { - error: {message: "Missing capability"}, + error: { message: "Missing capability" }, }); } else if (this.turnServers) { // We're already polling, so this is a no-op @@ -703,7 +723,7 @@ export class ClientWidgetApi extends EventEmitter { } catch (e) { console.error("error getting first TURN server results", e); await this.transport.reply(request, { - error: {message: "TURN servers not available"}, + error: { message: "TURN servers not available" }, }); } } @@ -712,7 +732,7 @@ export class ClientWidgetApi extends EventEmitter { private async handleUnwatchTurnServers(request: IUnwatchTurnServersRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC3846TurnServers)) { await this.transport.reply(request, { - error: {message: "Missing capability"}, + error: { message: "Missing capability" }, }); } else if (!this.turnServers) { // We weren't polling anyways, so this is a no-op @@ -746,28 +766,30 @@ export class ClientWidgetApi extends EventEmitter { 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, + 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 => { + 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.canReceiveRoomEvent(e.type, (e.content as { msgtype?: string })["msgtype"]); } }); - return this.transport.reply( - request, - { - chunk, - prev_batch: result.prevBatch, - next_batch: result.nextBatch, - }, - ); + return this.transport.reply(request, { + chunk, + prev_batch: result.prevBatch, + next_batch: result.nextBatch, + }); } catch (e) { console.error("error getting the relations", e); this.handleDriverError(e, request, "Unexpected error while reading relations"); @@ -781,7 +803,7 @@ export class ClientWidgetApi extends EventEmitter { }); } - if (typeof request.data.search_term !== 'string') { + if (typeof request.data.search_term !== "string") { return this.transport.reply(request, { error: { message: "Invalid request - missing search term" }, }); @@ -794,21 +816,16 @@ export class ClientWidgetApi extends EventEmitter { } 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, - })), - }, - ); + 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); this.handleDriverError(e, request, "Unexpected error while searching in the user directory"); @@ -825,10 +842,7 @@ export class ClientWidgetApi extends EventEmitter { try { const result = await this.driver.getMediaConfig(); - return this.transport.reply( - request, - result, - ); + return this.transport.reply(request, result); } catch (e) { console.error("error while getting the media configuration", e); this.handleDriverError(e, request, "Unexpected error while getting the media configuration"); @@ -845,10 +859,9 @@ export class ClientWidgetApi extends EventEmitter { try { const result = await this.driver.uploadFile(request.data.file); - return this.transport.reply( - request, - { content_uri: result.contentUri }, - ); + return this.transport.reply(request, { + content_uri: result.contentUri, + }); } catch (e) { console.error("error while uploading a file", e); this.handleDriverError(e, request, "Unexpected error while uploading a file"); @@ -865,10 +878,7 @@ export class ClientWidgetApi extends EventEmitter { try { const result = await this.driver.downloadFile(request.data.content_uri); - return this.transport.reply( - request, - { file: result.file }, - ); + return this.transport.reply(request, { file: result.file }); } catch (e) { console.error("error while downloading a file", e); this.handleDriverError(e, request, "Unexpected error while downloading a file"); @@ -980,15 +990,13 @@ export class ClientWidgetApi extends EventEmitter { } public notifyModalWidgetButtonClicked(id: IModalWidgetOpenRequestDataButton["id"]): Promise { - return this.transport.send( - WidgetApiToWidgetAction.ButtonClicked, {id}, - ).then(); + return this.transport + .send(WidgetApiToWidgetAction.ButtonClicked, { id }) + .then(); } public notifyModalWidgetClose(data: IModalWidgetReturnData): Promise { - return this.transport.send( - WidgetApiToWidgetAction.CloseModalWidget, data, - ).then(); + return this.transport.send(WidgetApiToWidgetAction.CloseModalWidget, data).then(); } /** @@ -1087,10 +1095,9 @@ export class ClientWidgetApi extends EventEmitter { events.push(...stateKeyMap.values()); } } - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: events }, - ); + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: events, + }); } finally { this.flushRoomStateTask = null; } @@ -1105,38 +1112,42 @@ export class ClientWidgetApi extends EventEmitter { if (cap.kind === EventKind.State && cap.direction === EventDirection.Receive) { // Initiate the task const events = this.driver.readRoomState(roomId, cap.eventType, cap.keyStr ?? undefined); - const task = events.then( - events => { - // When complete, queue the resulting events to be - // pushed to the widget - for (const event of events) { - let eventTypeMap = this.pushRoomStateResult.get(roomId); - if (eventTypeMap === undefined) { - eventTypeMap = new Map(); - this.pushRoomStateResult.set(roomId, eventTypeMap); - } - let stateKeyMap = eventTypeMap.get(cap.eventType); - if (stateKeyMap === undefined) { - stateKeyMap = new Map(); - eventTypeMap.set(cap.eventType, stateKeyMap); + const task = events + .then( + (events) => { + // When complete, queue the resulting events to be + // pushed to the widget + for (const event of events) { + let eventTypeMap = this.pushRoomStateResult.get(roomId); + if (eventTypeMap === undefined) { + eventTypeMap = new Map(); + this.pushRoomStateResult.set(roomId, eventTypeMap); + } + let stateKeyMap = eventTypeMap.get(cap.eventType); + if (stateKeyMap === undefined) { + stateKeyMap = new Map(); + eventTypeMap.set(cap.eventType, stateKeyMap); + } + if (!stateKeyMap.has(event.state_key!)) stateKeyMap.set(event.state_key!, event); } - if (!stateKeyMap.has(event.state_key!)) stateKeyMap.set(event.state_key!, event); - } - }, - e => console.error(`Failed to read room state for ${roomId} (${ - cap.eventType - }, ${cap.keyStr})`, e), - ).then(() => { - // Mark request as no longer pending - this.pushRoomStateTasks.delete(task); - }); + }, + (e) => + console.error( + `Failed to read room state for ${roomId} (${cap.eventType}, ${cap.keyStr})`, + e, + ), + ) + .then(() => { + // Mark request as no longer pending + this.pushRoomStateTasks.delete(task); + }); // Mark task as pending this.pushRoomStateTasks.add(task); // Assuming no other tasks are already happening concurrently, // schedule the widget action that actually pushes the events this.flushRoomStateTask ??= this.flushRoomState(); - this.flushRoomStateTask.catch(e => console.error('Failed to push room state', e)); + this.flushRoomStateTask.catch((e) => console.error("Failed to push room state", e)); } } } @@ -1152,18 +1163,17 @@ export class ClientWidgetApi extends EventEmitter { widget failed to handle the update. */ public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) throw new Error('Not a state event'); + if (rawEvent.state_key === undefined) throw new Error("Not a state event"); if ( - (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) - && this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) + (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) && + this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) ) { // Updates could race with the initial push of the room's state if (this.pushRoomStateTasks.size === 0) { // No initial push tasks are pending; safe to send immediately - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: [rawEvent] }, - ); + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: [rawEvent], + }); } else { // Lump the update in with whatever data will be sent in the // initial push later. Even if we set it to an "outdated" entry diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 508682a..44f0de9 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -67,7 +67,7 @@ import { IReadRoomAccountDataFromWidgetResponseData, } from "./interfaces/ReadRoomAccountDataAction"; import { IRoomEvent } from "./interfaces/IRoomEvent"; -import {IRoomAccountData} from "./interfaces/IRoomAccountData"; +import { IRoomAccountData } from "./interfaces/IRoomAccountData"; import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions"; import { Symbols } from "./Symbols"; import { @@ -142,17 +142,15 @@ export class WidgetApi extends EventEmitter { * 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) { + 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 = new PostmessageTransport(WidgetApiDirection.FromWidget, widgetId, window.parent, window); this.transport.targetOrigin = clientOrigin; this.transport.on("message", this.handleMessage.bind(this)); } @@ -193,7 +191,7 @@ export class WidgetApi extends EventEmitter { * @throws Throws if the capabilities negotiation has already started. */ public requestCapabilities(capabilities: Capability[]): void { - capabilities.forEach(cap => this.requestCapability(cap)); + capabilities.forEach((cap) => this.requestCapability(cap)); } /** @@ -309,40 +307,44 @@ export class WidgetApi extends EventEmitter { */ public requestOpenIDConnectToken(): Promise { return new Promise((resolve, reject) => { - this.transport.sendComplete( - 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): void => { - 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, {}); // ack - } else if (request.data.state === OpenIDRequestState.Blocked) { - reject(new Error("User declined to verify their identity")); - this.transport.reply(request, {}); // ack - } else { - reject(new Error("Invalid state on reply: " + rdata.state)); - this.transport.reply(request, { - 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); + this.transport + .sendComplete( + 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): void => { + 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, {}); // ack + } else if (request.data.state === OpenIDRequestState.Blocked) { + reject(new Error("User declined to verify their identity")); + this.transport.reply(request, {}); // ack + } else { + reject(new Error("Invalid state on reply: " + rdata.state)); + this.transport.reply(request, { + 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); }); } @@ -354,10 +356,11 @@ export class WidgetApi extends EventEmitter { * Use the WidgetApiToWidgetAction.NotifyCapabilities action to detect changes. */ public updateRequestedCapabilities(): Promise { - return this.transport.send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, - { + return this.transport + .send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, { capabilities: this.requestedCapabilities, - }).then(); + }) + .then(); } /** @@ -384,9 +387,12 @@ export class WidgetApi extends EventEmitter { * the request, resolves to false otherwise. Rejects if an error occurred. */ public setAlwaysOnScreen(value: boolean): Promise { - return this.transport.send( - WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, {value}, - ).then(res => res.success); + return this.transport + .send< + IStickyActionRequestData, + IStickyActionResponseData + >(WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, { value }) + .then((res) => res.success); } /** @@ -405,9 +411,15 @@ export class WidgetApi extends EventEmitter { data: IModalWidgetCreateData = {}, type: WidgetType = MatrixWidgetType.Custom, ): Promise { - return this.transport.send( - WidgetApiFromWidgetAction.OpenModalWidget, { type, url, name, buttons, data }, - ).then(); + return this.transport + .send(WidgetApiFromWidgetAction.OpenModalWidget, { + type, + url, + name, + buttons, + data, + }) + .then(); } /** @@ -491,15 +503,12 @@ export class WidgetApi extends EventEmitter { ): Promise { return this.transport.send( WidgetApiFromWidgetAction.SendToDevice, - {type: eventType, encrypted, messages: contentMap}, + { type: eventType, encrypted, messages: contentMap }, ); } - public readRoomAccountData( - eventType: string, - roomIds?: (string | Symbols.AnyRoom)[], - ): Promise { - const data: IReadEventFromWidgetRequestData = {type: eventType}; + public readRoomAccountData(eventType: string, roomIds?: (string | Symbols.AnyRoom)[]): Promise { + const data: IReadEventFromWidgetRequestData = { type: eventType }; if (roomIds) { if (roomIds.includes(Symbols.AnyRoom)) { @@ -508,13 +517,12 @@ export class WidgetApi extends EventEmitter { data.room_ids = roomIds; } } - return this.transport.send< - IReadRoomAccountDataFromWidgetRequestData, - IReadRoomAccountDataFromWidgetResponseData - >( - WidgetApiFromWidgetAction.BeeperReadRoomAccountData, - data, - ).then(r => r.events); + return this.transport + .send< + IReadRoomAccountDataFromWidgetRequestData, + IReadRoomAccountDataFromWidgetResponseData + >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) + .then((r) => r.events); } public readRoomEvents( @@ -524,7 +532,7 @@ export class WidgetApi extends EventEmitter { roomIds?: (string | Symbols.AnyRoom)[], since?: string | undefined, ): Promise { - const data: IReadEventFromWidgetRequestData = {type: eventType, msgtype: msgtype}; + const data: IReadEventFromWidgetRequestData = { type: eventType, msgtype: msgtype }; if (limit !== undefined) { data.limit = limit; } @@ -538,10 +546,12 @@ export class WidgetApi extends EventEmitter { if (since) { data.since = since; } - return this.transport.send( - WidgetApiFromWidgetAction.MSC2876ReadEvents, - data, - ).then(r => r.events); + return this.transport + .send< + IReadEventFromWidgetRequestData, + IReadEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC2876ReadEvents, data) + .then((r) => r.events); } /** @@ -571,7 +581,7 @@ export class WidgetApi extends EventEmitter { limit?: number, from?: string, to?: string, - direction?: 'f' | 'b', + direction?: "f" | "b", ): Promise { const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC3869)) { @@ -615,10 +625,12 @@ export class WidgetApi extends EventEmitter { data.room_ids = roomIds; } } - return this.transport.send( - WidgetApiFromWidgetAction.MSC2876ReadEvents, - data, - ).then(r => r.events); + return this.transport + .send< + IReadEventFromWidgetRequestData, + IReadEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC2876ReadEvents, data) + .then((r) => r.events); } /** @@ -632,9 +644,12 @@ export class WidgetApi extends EventEmitter { if (buttonId === BuiltInModalButtonID.Close) { throw new Error("The close button cannot be disabled"); } - return this.transport.send( - WidgetApiFromWidgetAction.SetModalButtonEnabled, {button: buttonId, enabled: isEnabled}, - ).then(); + return this.transport + .send(WidgetApiFromWidgetAction.SetModalButtonEnabled, { + button: buttonId, + enabled: isEnabled, + }) + .then(); } /** @@ -650,9 +665,9 @@ export class WidgetApi extends EventEmitter { throw new Error("Invalid matrix.to URI"); } - return this.transport.send( - WidgetApiFromWidgetAction.MSC2931Navigate, {uri}, - ).then(); + return this.transport + .send(WidgetApiFromWidgetAction.MSC2931Navigate, { uri }) + .then(); } /** @@ -660,7 +675,7 @@ export class WidgetApi extends EventEmitter { * 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 { + public async *getTurnServers(): AsyncGenerator { let setTurnServer: (server: ITurnServer) => void; const onUpdateTurnServers = async (ev: CustomEvent): Promise => { @@ -687,7 +702,7 @@ export class WidgetApi extends EventEmitter { try { // Watch for new data indefinitely (until this generator's return method is called) while (true) { - yield await new Promise(resolve => setTurnServer = resolve); + yield await new Promise((resolve) => (setTurnServer = resolve)); } } finally { // The loop was broken by the caller - clean up @@ -762,10 +777,10 @@ export class WidgetApi extends EventEmitter { file, }; - return this.transport.send< - IUploadFileActionFromWidgetRequestData, - IUploadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data); + return this.transport.send( + WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data, + ); } /** @@ -783,10 +798,10 @@ export class WidgetApi extends EventEmitter { content_uri: contentUri, }; - return this.transport.send< - IDownloadFileActionFromWidgetRequestData, - IDownloadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data); + return this.transport.send( + WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data, + ); } /** @@ -795,7 +810,7 @@ export class WidgetApi extends EventEmitter { */ public start(): void { this.transport.start(); - this.getClientVersions().then(v => { + this.getClientVersions().then((v) => { if (v.includes(UnstableApiVersion.MSC2974)) { this.supportsMSC2974Renegotiate = true; } @@ -839,15 +854,19 @@ export class WidgetApi extends EventEmitter { return Promise.resolve(this.cachedClientVersions); } - return this.transport.send( - 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 []; - }); + return this.transport + .send( + 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): void | Promise { @@ -860,7 +879,7 @@ export class WidgetApi extends EventEmitter { } // See if we can expect a capabilities notification or not - return this.getClientVersions().then(v => { + return this.getClientVersions().then((v) => { if (v.includes(UnstableApiVersion.MSC2871)) { this.once( `action:${WidgetApiToWidgetAction.NotifyCapabilities}`, diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index f638dac..df92c03 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -145,10 +145,7 @@ export abstract class WidgetDriver { * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. * @throws Rejected when there is no matching delayed event, or when the action failed to run. */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { + public updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { return Promise.reject(new Error("Failed to override function")); } @@ -178,10 +175,7 @@ export abstract class WidgetDriver { * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. * @returns {Promise} Resolves to the element of room account data, or an empty array. */ - public readRoomAccountData( - eventType: string, - roomIds: string[] | null = null, - ): Promise { + public readRoomAccountData(eventType: string, roomIds: string[] | null = null): Promise { return Promise.resolve([]); } @@ -281,11 +275,7 @@ export abstract class WidgetDriver { * @returns {Promise} Resolves to the events representing the * current values of the room state entries. */ - public readRoomState( - roomId: string, - eventType: string, - stateKey: string | undefined, - ): Promise { + public readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise { return Promise.resolve([]); } @@ -321,7 +311,7 @@ export abstract class WidgetDriver { from?: string, to?: string, limit?: number, - direction?: 'f' | 'b', + direction?: "f" | "b", ): Promise { return Promise.resolve({ chunk: [] }); } @@ -340,7 +330,7 @@ export abstract class WidgetDriver { * @param {SimpleObservable} observer The observable to feed updates into. */ public askOpenID(observer: SimpleObservable): void { - observer.update({state: OpenIDRequestState.Blocked}); + observer.update({ state: OpenIDRequestState.Blocked }); } /** @@ -372,10 +362,7 @@ export abstract class WidgetDriver { * @param limit The maximum number of results to return. If not supplied, the * @returns Resolves to the search results. */ - public searchUserDirectory( - searchTerm: string, - limit?: number, - ): Promise { + public searchUserDirectory(searchTerm: string, limit?: number): Promise { return Promise.resolve({ limited: false, results: [] }); } @@ -393,9 +380,7 @@ export abstract class WidgetDriver { * XMLHttpRequest.send (typically a File). * @returns Resolves to the location of the uploaded file. */ - public uploadFile( - file: XMLHttpRequestBodyInit, - ): Promise<{ contentUri: string }> { + public uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }> { throw new Error("Upload file is not implemented"); } @@ -404,9 +389,7 @@ export abstract class WidgetDriver { * @param contentUri - MXC URI of the file to download. * @returns Resolves to the contents of the file. */ - public downloadFile( - contentUri: string, - ): Promise<{ file: XMLHttpRequestBodyInit }> { + public downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }> { throw new Error("Download file is not implemented"); } diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index 9baaae1..f541ac5 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -31,24 +31,24 @@ export enum MatrixCapabilities { MSC2931Navigate = "org.matrix.msc2931.navigate", MSC3846TurnServers = "town.robin.msc3846.turn_servers", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ MSC4039UploadFile = "org.matrix.msc4039.upload_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ MSC4039DownloadFile = "org.matrix.msc4039.download_file", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } diff --git a/src/interfaces/DownloadFileAction.ts b/src/interfaces/DownloadFileAction.ts index 48ba6dd..f3eed2e 100644 --- a/src/interfaces/DownloadFileAction.ts +++ b/src/interfaces/DownloadFileAction.ts @@ -18,23 +18,19 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IDownloadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - content_uri: string; // eslint-disable-line camelcase +export interface IDownloadFileActionFromWidgetRequestData extends IWidgetApiRequestData { + content_uri: string; // eslint-disable-line camelcase } -export interface IDownloadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; - data: IDownloadFileActionFromWidgetRequestData; +export interface IDownloadFileActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; + data: IDownloadFileActionFromWidgetRequestData; } -export interface IDownloadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - file: XMLHttpRequestBodyInit; +export interface IDownloadFileActionFromWidgetResponseData extends IWidgetApiResponseData { + file: XMLHttpRequestBodyInit; } -export interface IDownloadFileActionFromWidgetActionResponse - extends IDownloadFileActionFromWidgetActionRequest { - response: IDownloadFileActionFromWidgetResponseData; +export interface IDownloadFileActionFromWidgetActionResponse extends IDownloadFileActionFromWidgetActionRequest { + response: IDownloadFileActionFromWidgetResponseData; } diff --git a/src/interfaces/GetMediaConfigAction.ts b/src/interfaces/GetMediaConfigAction.ts index f67c2c8..71f19d1 100644 --- a/src/interfaces/GetMediaConfigAction.ts +++ b/src/interfaces/GetMediaConfigAction.ts @@ -18,21 +18,17 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IGetMediaConfigActionFromWidgetRequestData - extends IWidgetApiRequestData {} +export interface IGetMediaConfigActionFromWidgetRequestData extends IWidgetApiRequestData {} -export interface IGetMediaConfigActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; - data: IGetMediaConfigActionFromWidgetRequestData; +export interface IGetMediaConfigActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; + data: IGetMediaConfigActionFromWidgetRequestData; } -export interface IGetMediaConfigActionFromWidgetResponseData - extends IWidgetApiResponseData { - "m.upload.size"?: number; +export interface IGetMediaConfigActionFromWidgetResponseData extends IWidgetApiResponseData { + "m.upload.size"?: number; } -export interface IGetMediaConfigActionFromWidgetActionResponse - extends IGetMediaConfigActionFromWidgetActionRequest { - response: IGetMediaConfigActionFromWidgetResponseData; +export interface IGetMediaConfigActionFromWidgetActionResponse extends IGetMediaConfigActionFromWidgetActionRequest { + response: IGetMediaConfigActionFromWidgetResponseData; } diff --git a/src/interfaces/IWidgetApiErrorResponse.ts b/src/interfaces/IWidgetApiErrorResponse.ts index 89a29de..a215c2a 100644 --- a/src/interfaces/IWidgetApiErrorResponse.ts +++ b/src/interfaces/IWidgetApiErrorResponse.ts @@ -22,9 +22,9 @@ import { IWidgetApiResponse, IWidgetApiResponseData } from "./IWidgetApiResponse */ export interface IMatrixApiError { /** The HTTP status code of the associated request. */ - http_status: number; // eslint-disable-line camelcase + http_status: number; // eslint-disable-line camelcase /** Any HTTP response headers that are relevant to the error. */ - http_headers: {[name: string]: string}; // eslint-disable-line camelcase + http_headers: { [name: string]: string }; // eslint-disable-line camelcase /** The URL of the failed request. */ url: string; /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ @@ -36,7 +36,7 @@ export interface IMatrixApiError { export interface IWidgetApiErrorResponseDataDetails { /** Set if the error came from a Matrix API request made by a widget driver */ - matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase + matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase } export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData { @@ -52,6 +52,5 @@ export interface IWidgetApiErrorResponse extends IWidgetApiResponse { export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData { const error = responseData.error; - return typeof error === "object" && error !== null && - "message" in error && typeof error.message === "string"; + return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"; } diff --git a/src/interfaces/LanguageChangeAction.ts b/src/interfaces/LanguageChangeAction.ts index f2453c0..8b5de3a 100644 --- a/src/interfaces/LanguageChangeAction.ts +++ b/src/interfaces/LanguageChangeAction.ts @@ -19,10 +19,10 @@ import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface ILanguageChangeActionRequestData extends IWidgetApiRequestData { - /** - * The BCP 47 identifier for the client's current language. - */ - lang: string; + /** + * The BCP 47 identifier for the client's current language. + */ + lang: string; } export interface ILanguageChangeActionRequest extends IWidgetApiRequest { diff --git a/src/interfaces/ReadRelationsAction.ts b/src/interfaces/ReadRelationsAction.ts index 76a041a..d89d538 100644 --- a/src/interfaces/ReadRelationsAction.ts +++ b/src/interfaces/ReadRelationsAction.ts @@ -28,7 +28,7 @@ export interface IReadRelationsFromWidgetRequestData extends IWidgetApiRequestDa limit?: number; from?: string; to?: string; - direction?: 'f' | 'b'; + direction?: "f" | "b"; } export interface IReadRelationsFromWidgetActionRequest extends IWidgetApiRequest { diff --git a/src/interfaces/SendEventAction.ts b/src/interfaces/SendEventAction.ts index 45963ad..4631dac 100644 --- a/src/interfaces/SendEventAction.ts +++ b/src/interfaces/SendEventAction.ts @@ -47,8 +47,7 @@ export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidget response: ISendEventFromWidgetResponseData; } -export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent { -} +export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {} export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest { action: WidgetApiToWidgetAction.SendEvent; diff --git a/src/interfaces/StickerAction.ts b/src/interfaces/StickerAction.ts index 13cb94a..c7293e3 100644 --- a/src/interfaces/StickerAction.ts +++ b/src/interfaces/StickerAction.ts @@ -28,7 +28,8 @@ export interface IStickerActionRequestData extends IWidgetApiRequestData { w?: number; mimetype?: string; size?: number; - thumbnail_info?: { // eslint-disable-line camelcase + thumbnail_info?: { + // eslint-disable-line camelcase h?: number; w?: number; mimetype?: string; diff --git a/src/interfaces/ThemeChangeAction.ts b/src/interfaces/ThemeChangeAction.ts index 30138e9..292f58e 100644 --- a/src/interfaces/ThemeChangeAction.ts +++ b/src/interfaces/ThemeChangeAction.ts @@ -19,7 +19,7 @@ import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IThemeChangeActionRequestData extends IWidgetApiRequestData { - // The format of a theme is deliberately unstandardized + // The format of a theme is deliberately unstandardized } export interface IThemeChangeActionRequest extends IWidgetApiRequest { diff --git a/src/interfaces/TurnServerActions.ts b/src/interfaces/TurnServerActions.ts index 3a9bd29..36f664a 100644 --- a/src/interfaces/TurnServerActions.ts +++ b/src/interfaces/TurnServerActions.ts @@ -42,8 +42,7 @@ export interface IUnwatchTurnServersResponse extends IWidgetApiResponse { response: IWidgetApiAcknowledgeResponseData; } -export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer { -} +export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer {} export interface IUpdateTurnServersRequest extends IWidgetApiRequest { action: WidgetApiToWidgetAction.UpdateTurnServers; diff --git a/src/interfaces/UploadFileAction.ts b/src/interfaces/UploadFileAction.ts index 86d529f..9d120b6 100644 --- a/src/interfaces/UploadFileAction.ts +++ b/src/interfaces/UploadFileAction.ts @@ -18,23 +18,19 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IUploadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - file: XMLHttpRequestBodyInit; +export interface IUploadFileActionFromWidgetRequestData extends IWidgetApiRequestData { + file: XMLHttpRequestBodyInit; } -export interface IUploadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; - data: IUploadFileActionFromWidgetRequestData; +export interface IUploadFileActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; + data: IUploadFileActionFromWidgetRequestData; } -export interface IUploadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - content_uri: string; // eslint-disable-line camelcase +export interface IUploadFileActionFromWidgetResponseData extends IWidgetApiResponseData { + content_uri: string; // eslint-disable-line camelcase } -export interface IUploadFileActionFromWidgetActionResponse - extends IUploadFileActionFromWidgetActionRequest { - response: IUploadFileActionFromWidgetResponseData; +export interface IUploadFileActionFromWidgetActionResponse extends IUploadFileActionFromWidgetActionRequest { + response: IUploadFileActionFromWidgetResponseData; } diff --git a/src/models/WidgetEventCapability.ts b/src/models/WidgetEventCapability.ts index 93998c2..1190606 100644 --- a/src/models/WidgetEventCapability.ts +++ b/src/models/WidgetEventCapability.ts @@ -35,8 +35,7 @@ export class WidgetEventCapability { public readonly kind: EventKind, public readonly keyStr: string | null, public readonly raw: string, - ) { - } + ) {} public matchesAsStateEvent(direction: EventDirection, eventType: string, stateKey: string | null): boolean { if (this.kind !== EventKind.State) return false; // not a state event @@ -90,8 +89,8 @@ export class WidgetEventCapability { ): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/22 - eventType = eventType.replace(/#/g, '\\#'); - stateKey = stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ''; + eventType = eventType.replace(/#/g, "\\#"); + stateKey = stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ""; const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}`; // cheat by sending it through the processor @@ -119,7 +118,7 @@ export class WidgetEventCapability { public static forRoomMessageEvent(direction: EventDirection, msgtype?: string): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/22 - msgtype = msgtype === null || msgtype === undefined ? '' : msgtype; + msgtype = msgtype === null || msgtype === undefined ? "" : msgtype; const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}`; // cheat by sending it through the processor @@ -186,7 +185,7 @@ export class WidgetEventCapability { // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". const expectingKeyStr = eventSegment.startsWith("m.room.message#") || kind === EventKind.State; let keyStr: string | null = null; - if (eventSegment.includes('#') && expectingKeyStr) { + if (eventSegment.includes("#") && expectingKeyStr) { // Dev note: regex is difficult to write, so instead the rules are manually written // out. This is probably just as understandable as a boring regex though, so win-win? @@ -202,19 +201,20 @@ export class WidgetEventCapability { // m.room.message\\###test m.room.message\# #test // First step: explode the string - const parts = eventSegment.split('#'); + const parts = eventSegment.split("#"); // To form the eventSegment, we'll keep finding parts of the exploded string until // there's one that doesn't end with the escape character (\). We'll then join those // segments together with the exploding character. We have to remember to consume the // escape character as well. - const idx = parts.findIndex(p => !p.endsWith("\\")); - eventSegment = parts.slice(0, idx + 1) - .map(p => p.endsWith('\\') ? p.substring(0, p.length - 1) : p) - .join('#'); + const idx = parts.findIndex((p) => !p.endsWith("\\")); + eventSegment = parts + .slice(0, idx + 1) + .map((p) => (p.endsWith("\\") ? p.substring(0, p.length - 1) : p)) + .join("#"); // The keyStr is whatever is left over. - keyStr = parts.slice(idx + 1).join('#'); + keyStr = parts.slice(idx + 1).join("#"); } parsed.push(new WidgetEventCapability(direction, eventSegment, kind, keyStr, cap)); diff --git a/src/models/WidgetParser.ts b/src/models/WidgetParser.ts index f93c077..07ced72 100644 --- a/src/models/WidgetParser.ts +++ b/src/models/WidgetParser.ts @@ -116,17 +116,17 @@ export class WidgetParser { // is done against the requirements of the interface because not everyone // will have an interface to validate against. - const content = stateEvent.content as IWidget || {}; + const content = (stateEvent.content as IWidget) || {}; // Form our best approximation of a widget with the information we have const estimatedWidget: IWidget = { id: stateEvent.state_key, - creatorUserId: content['creatorUserId'] || stateEvent.sender, - name: content['name'], - type: content['type'], - url: content['url'], - waitForIframeLoad: content['waitForIframeLoad'], - data: content['data'], + creatorUserId: content["creatorUserId"] || stateEvent.sender, + name: content["name"], + type: content["type"], + url: content["url"], + waitForIframeLoad: content["waitForIframeLoad"], + data: content["data"], }; // Finally, process that widget diff --git a/src/templating/url-template.ts b/src/templating/url-template.ts index 6bf4ffa..b700a9b 100644 --- a/src/templating/url-template.ts +++ b/src/templating/url-template.ts @@ -31,28 +31,28 @@ export interface ITemplateParams { export function runTemplate(url: string, widget: IWidget, params: ITemplateParams): string { // Always apply the supplied params over top of data to ensure the data can't lie about them. const variables = Object.assign({}, widget.data, { - 'matrix_room_id': params.widgetRoomId || "", - 'matrix_user_id': params.currentUserId, - 'matrix_display_name': params.userDisplayName || params.currentUserId, - 'matrix_avatar_url': params.userHttpAvatarUrl || "", - 'matrix_widget_id': widget.id, + "matrix_room_id": params.widgetRoomId || "", + "matrix_user_id": params.currentUserId, + "matrix_display_name": params.userDisplayName || params.currentUserId, + "matrix_avatar_url": params.userHttpAvatarUrl || "", + "matrix_widget_id": widget.id, // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) - 'org.matrix.msc2873.client_id': params.clientId || "", - 'org.matrix.msc2873.client_theme': params.clientTheme || "", - 'org.matrix.msc2873.client_language': params.clientLanguage || "", + "org.matrix.msc2873.client_id": params.clientId || "", + "org.matrix.msc2873.client_theme": params.clientTheme || "", + "org.matrix.msc2873.client_language": params.clientLanguage || "", // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) - 'org.matrix.msc3819.matrix_device_id': params.deviceId || "", + "org.matrix.msc3819.matrix_device_id": params.deviceId || "", // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) - 'org.matrix.msc4039.matrix_base_url': params.baseUrl || "", + "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", }); let result = url; for (const key of Object.keys(variables)) { // Regex escape from https://stackoverflow.com/a/6969486/7037379 - const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - const rexp = new RegExp(pattern, 'g'); + const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + const rexp = new RegExp(pattern, "g"); // This is technically not what we're supposed to do for a couple of reasons: // 1. We are assuming that there won't later be a $key match after we replace a variable. diff --git a/src/transport/ITransport.ts b/src/transport/ITransport.ts index b75cf9b..3446e6a 100644 --- a/src/transport/ITransport.ts +++ b/src/transport/ITransport.ts @@ -81,7 +81,7 @@ export interface ITransport extends EventEmitter { */ send( action: WidgetApiAction, - data: T + data: T, ): Promise; /** @@ -95,7 +95,10 @@ export interface ITransport extends EventEmitter { * @throws {WidgetApiResponseError} if the request failed with error details * that can be communicated to the Widget API. */ - sendComplete(action: WidgetApiAction, data: T): Promise; + sendComplete( + action: WidgetApiAction, + data: T, + ): Promise; /** * Replies to a request. diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index aede279..4589735 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -94,13 +94,15 @@ export class PostmessageTransport extends EventEmitter implements ITransport { } public send( - action: WidgetApiAction, data: T, + action: WidgetApiAction, + data: T, ): Promise { - return this.sendComplete(action, data).then(r => r.response); + return this.sendComplete(action, data).then((r) => r.response); } public sendComplete( - action: WidgetApiAction, data: T, + action: WidgetApiAction, + data: T, ): Promise { if (!this.ready || !this.widgetId) { return Promise.reject(new Error("Not ready or unknown widget ID")); @@ -113,7 +115,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { data: data, }; if (action === WidgetApiToWidgetAction.UpdateVisibility) { - request['visible'] = data['visible']; + request["visible"] = data["visible"]; } return new Promise((prResolve, prReject) => { const resolve = (response: IWidgetApiResponse): void => { @@ -125,10 +127,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { prReject(err); }; - const timerId = setTimeout( - () => reject(new Error("Request timed out")), - (this.timeoutSeconds || 1) * 1000, - ); + const timerId = setTimeout(() => reject(new Error("Request timed out")), (this.timeoutSeconds || 1) * 1000); const onStop = (): void => reject(new Error("Transport stopped")); this.stopController.signal.addEventListener("abort", onStop); @@ -185,7 +184,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { this._widgetId = request.widgetId; } - this.emit("message", new CustomEvent("message", {detail: request})); + this.emit("message", new CustomEvent("message", { detail: request })); } private handleResponse(response: IWidgetApiResponse): void { @@ -195,7 +194,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { if (!req) return; // response to an unknown request if (isErrorResponse(response.response)) { - const {message, ...data} = response.response.error; + const { message, ...data } = response.response.error; req.reject(new WidgetApiResponseError(message, data)); } else { req.resolve(response); diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index fd446d7..15bec9e 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -17,8 +17,8 @@ import { waitFor } from '@testing-library/dom'; -import { ClientWidgetApi } from "../src/ClientWidgetApi"; -import { WidgetDriver } from "../src/driver/WidgetDriver"; +import { ClientWidgetApi } from '../src/ClientWidgetApi'; +import { WidgetDriver } from '../src/driver/WidgetDriver'; import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; import { Capability } from '../src/interfaces/Capabilities'; import { IRoomEvent } from '../src/interfaces/IRoomEvent'; @@ -80,18 +80,20 @@ class CustomMatrixError extends Error { } function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetails | undefined { - return e instanceof CustomMatrixError ? { - matrix_api_error: { - http_status: e.httpStatus, - http_headers: {}, - url: '', - response: { - errcode: e.name, - error: e.message, - ...e.data, - }, - }, - } : undefined; + return e instanceof CustomMatrixError + ? { + matrix_api_error: { + http_status: e.httpStatus, + http_headers: {}, + url: '', + response: { + errcode: e.name, + error: e.message, + ...e.data, + }, + }, + } + : undefined; } describe('ClientWidgetApi', () => { @@ -100,12 +102,12 @@ describe('ClientWidgetApi', () => { let driver: jest.Mocked; let clientWidgetApi: ClientWidgetApi; let transport: PostmessageTransport; - let emitEvent: Parameters["1"]; + let emitEvent: Parameters['1']; async function loadIframe(caps: Capability[] = []): Promise { capabilities = caps; - const ready = new Promise(resolve => { + const ready = new Promise((resolve) => { clientWidgetApi.once('ready', resolve); }); @@ -141,10 +143,10 @@ describe('ClientWidgetApi', () => { clientWidgetApi = new ClientWidgetApi( new Widget({ - id: "test", - creatorUserId: "@alice:example.org", - type: "example", - url: "https://example.org", + id: 'test', + creatorUserId: '@alice:example.org', + type: 'example', + url: 'https://example.org', }), iframe, driver, @@ -154,9 +156,7 @@ describe('ClientWidgetApi', () => { emitEvent = jest.mocked(transport.on).mock.calls[0][1]; jest.mocked(transport.send).mockResolvedValue({}); - jest.mocked(driver.validateCapabilities).mockImplementation( - async () => new Set(capabilities), - ); + jest.mocked(driver.validateCapabilities).mockImplementation(async () => new Set(capabilities)); }); afterEach(() => { @@ -193,9 +193,7 @@ describe('ClientWidgetApi', () => { expect(transport.reply).toHaveBeenCalledWith(event, {}); }); - expect(driver.navigate).toHaveBeenCalledWith( - event.data.uri, - ); + expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); }); it('fails to navigate', async () => { @@ -247,9 +245,7 @@ describe('ClientWidgetApi', () => { }); it('should reject requests when the driver throws an exception', async () => { - driver.navigate.mockRejectedValue( - new Error("M_UNKNOWN: Unknown error"), - ); + driver.navigate.mockRejectedValue(new Error('M_UNKNOWN: Unknown error')); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, @@ -276,14 +272,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.navigate.mockRejectedValue( - new CustomMatrixError( - 'failed to navigate', - 400, - 'M_UNKNOWN', - { - reason: 'Unknown error', - }, - ), + new CustomMatrixError('failed to navigate', 400, 'M_UNKNOWN', { + reason: 'Unknown error', + }), ); const event: INavigateActionRequest = { @@ -356,12 +347,7 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - null, - roomId, - ); + expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, null, roomId); }); it('sends state events', async () => { @@ -400,20 +386,13 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - '', - roomId, - ); + expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, '', roomId); }); it('should reject requests when the driver throws an exception', async () => { const roomId = '!room:example.org'; - driver.sendEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); + driver.sendEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -447,14 +426,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.sendEvent.mockRejectedValue( - new CustomMatrixError( - 'failed to send event', - 400, - 'M_NOT_JSON', - { - reason: 'Content must be a JSON object.', - }, - ), + new CustomMatrixError('failed to send event', 400, 'M_NOT_JSON', { + reason: 'Content must be a JSON object.', + }), ); const event: ISendEventFromWidgetActionRequest = { @@ -632,9 +606,7 @@ describe('ClientWidgetApi', () => { it('should reject requests when the driver throws an exception', async () => { const roomId = '!room:example.org'; - driver.sendDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); + driver.sendDelayedEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -671,14 +643,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.sendDelayedEvent.mockRejectedValue( - new CustomMatrixError( - 'failed to send event', - 400, - 'M_NOT_JSON', - { - reason: 'Content must be a JSON object.', - }, - ), + new CustomMatrixError('failed to send event', 400, 'M_NOT_JSON', { + reason: 'Content must be a JSON object.', + }), ); const event: ISendEventFromWidgetActionRequest = { @@ -819,7 +786,7 @@ describe('ClientWidgetApi', () => { // Artificially delay the delivery of the join rules event let resolveJoinRules: () => void; - const joinRules = new Promise(resolve => resolveJoinRules = resolve); + const joinRules = new Promise((resolve) => (resolveJoinRules = resolve)); driver.readRoomState.mockImplementation(async (rId, eventType, stateKey) => { if (rId === roomId) { @@ -856,15 +823,13 @@ describe('ClientWidgetApi', () => { await waitFor(() => { // The initial topic and name should have been pushed - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { state: [topicEvent, nameEvent, newJoinRulesEvent] }, - ); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: [topicEvent, nameEvent, newJoinRulesEvent], + }); // Only the updated join rules should have been delivered - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { state: expect.arrayContaining([joinRules]) }, - ); + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([joinRules]), + }); }); // Check that further updates to room state are pushed to the widget @@ -878,28 +843,25 @@ describe('ClientWidgetApi', () => { clientWidgetApi.feedStateUpdate(newTopicEvent); await waitFor(() => { - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { state: [newTopicEvent] }, - ); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: [newTopicEvent], + }); }); // Up to this point we should not have received any state for the // other (unviewed) room - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { state: expect.arrayContaining([otherRoomNameEvent]) }, - ); + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([otherRoomNameEvent]), + }); // Now view the other room clientWidgetApi.setViewedRoomId(otherRoomId); (transport.send as unknown as jest.SpyInstance).mockClear(); await waitFor(() => { // The state of the other room should now be pushed - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { state: expect.arrayContaining([otherRoomNameEvent]) }, - ); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([otherRoomNameEvent]), + }); }); }); }); @@ -982,17 +944,12 @@ describe('ClientWidgetApi', () => { expect(transport.reply).toHaveBeenCalledWith(event, {}); }); - expect(driver.updateDelayedEvent).toHaveBeenCalledWith( - event.data.delay_id, - event.data.action, - ); + expect(driver.updateDelayedEvent).toHaveBeenCalledWith(event.data.delay_id, event.data.action); } }); it('should reject requests when the driver throws an exception', async () => { - driver.updateDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); + driver.updateDelayedEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1020,14 +977,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.updateDelayedEvent.mockRejectedValue( - new CustomMatrixError( - 'failed to update delayed event', - 400, - 'M_NOT_JSON', - { - reason: 'Content must be a JSON object.', - }, - ), + new CustomMatrixError('failed to update delayed event', 400, 'M_NOT_JSON', { + reason: 'Content must be a JSON object.', + }), ); const event: IUpdateDelayedEventFromWidgetActionRequest = { @@ -1077,8 +1029,8 @@ describe('ClientWidgetApi', () => { encrypted: false, messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1110,8 +1062,8 @@ describe('ClientWidgetApi', () => { encrypted: false, messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1166,8 +1118,8 @@ describe('ClientWidgetApi', () => { type: 'net.example.test', messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1198,8 +1150,8 @@ describe('ClientWidgetApi', () => { encrypted: false, messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1234,8 +1186,8 @@ describe('ClientWidgetApi', () => { encrypted: false, messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1257,14 +1209,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.sendToDevice.mockRejectedValue( - new CustomMatrixError( - 'failed to send event', - 400, - 'M_FORBIDDEN', - { - reason: "You don't have permission to send to-device events", - }, - ), + new CustomMatrixError('failed to send event', 400, 'M_FORBIDDEN', { + reason: "You don't have permission to send to-device events", + }), ); const event: ISendToDeviceFromWidgetActionRequest = { @@ -1277,8 +1224,8 @@ describe('ClientWidgetApi', () => { encrypted: false, messages: { '@foo:bar.com': { - 'DEVICEID': { - 'example_content_key': 'value', + DEVICEID: { + example_content_key: 'value', }, }, }, @@ -1315,7 +1262,7 @@ describe('ClientWidgetApi', () => { observable.update({ state: OpenIDRequestState.Allowed, token: { - access_token: "access_token", + access_token: 'access_token', }, }); }); @@ -1335,7 +1282,7 @@ describe('ClientWidgetApi', () => { await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { state: OpenIDRequestState.Allowed, - access_token: "access_token", + access_token: 'access_token', }); }); @@ -1376,11 +1323,13 @@ describe('ClientWidgetApi', () => { const type = 'net.example.test'; const roomId = '!room:example.org'; - driver.readRoomAccountData.mockResolvedValue([{ - type, - room_id: roomId, - content: {}, - }]); + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]); const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1393,19 +1342,19 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - `com.beeper.capabilities.receive.room_account_data:${type}`, - ]); + await loadIframe([`com.beeper.capabilities.receive.room_account_data:${type}`]); emitEvent(new CustomEvent('', { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { - events: [{ - type, - room_id: roomId, - content: {}, - }], + events: [ + { + type, + room_id: roomId, + content: {}, + }, + ], }); }); @@ -1416,11 +1365,13 @@ describe('ClientWidgetApi', () => { const type = 'net.example.test'; const roomId = '!room:example.org'; - driver.readRoomAccountData.mockResolvedValue([{ - type, - room_id: roomId, - content: {}, - }]); + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]); const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1482,7 +1433,12 @@ describe('ClientWidgetApi', () => { }); expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, 'net.example.test', undefined, undefined, 0, undefined, + roomId, + 'net.example.test', + undefined, + undefined, + 0, + undefined, ); }); @@ -1524,10 +1480,20 @@ describe('ClientWidgetApi', () => { }); expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, 'net.example.test', undefined, undefined, 0, undefined, + roomId, + 'net.example.test', + undefined, + undefined, + 0, + undefined, ); expect(driver.readRoomTimeline).toHaveBeenCalledWith( - otherRoomId, 'net.example.test', undefined, undefined, 0, undefined, + otherRoomId, + 'net.example.test', + undefined, + undefined, + 0, + undefined, ); }); @@ -1563,7 +1529,12 @@ describe('ClientWidgetApi', () => { }); expect(driver.readRoomTimeline).toBeCalledWith( - '!room-id', 'net.example.test', undefined, undefined, 0, undefined, + '!room-id', + 'net.example.test', + undefined, + undefined, + 0, + undefined, ); }); @@ -1593,9 +1564,7 @@ describe('ClientWidgetApi', () => { }); it('reads state events with a specific state key', async () => { - driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: 'net.example.test', state_key: 'B' }), - ]); + driver.readRoomTimeline.mockResolvedValue([createRoomEvent({ type: 'net.example.test', state_key: 'B' })]); const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1615,14 +1584,17 @@ describe('ClientWidgetApi', () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - events: [ - createRoomEvent({ type: 'net.example.test', state_key: 'B' }), - ], + events: [createRoomEvent({ type: 'net.example.test', state_key: 'B' })], }); }); expect(driver.readRoomTimeline).toBeCalledWith( - '!room-id', 'net.example.test', undefined, 'B', 0, undefined, + '!room-id', + 'net.example.test', + undefined, + 'B', + 0, + undefined, ); }); @@ -1666,9 +1638,7 @@ describe('ClientWidgetApi', () => { emitEvent(new CustomEvent('', { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3869, - ]), + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3869]), }); }); @@ -1685,9 +1655,7 @@ describe('ClientWidgetApi', () => { data: { event_id: '$event' }, }; - await loadIframe([ - 'org.matrix.msc2762.receive.event:m.room.message', - ]); + await loadIframe(['org.matrix.msc2762.receive.event:m.room.message']); emitEvent(new CustomEvent('', { detail: event })); @@ -1698,8 +1666,14 @@ describe('ClientWidgetApi', () => { }); expect(driver.readEventRelations).toBeCalledWith( - '$event', undefined, undefined, undefined, undefined, undefined, - undefined, undefined, + '$event', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, ); }); @@ -1730,16 +1704,19 @@ describe('ClientWidgetApi', () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - chunk: [ - createRoomEvent(), - createRoomEvent({ type: 'net.example.test', state_key: 'A' }), - ], + chunk: [createRoomEvent(), createRoomEvent({ type: 'net.example.test', state_key: 'A' })], }); }); expect(driver.readEventRelations).toBeCalledWith( - '$event', undefined, undefined, undefined, undefined, undefined, - undefined, undefined, + '$event', + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, ); }); @@ -1765,9 +1742,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc2762.timeline:!room-id', - ]); + await loadIframe(['org.matrix.msc2762.timeline:!room-id']); emitEvent(new CustomEvent('', { detail: event })); @@ -1778,8 +1753,14 @@ describe('ClientWidgetApi', () => { }); expect(driver.readEventRelations).toBeCalledWith( - '$event', '!room-id', 'm.reference', 'm.room.message', - 'from-token', 'to-token', 25, 'f', + '$event', + '!room-id', + 'm.reference', + 'm.room.message', + 'from-token', + 'to-token', + 25, + 'f', ); }); @@ -1865,14 +1846,9 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.readEventRelations.mockRejectedValue( - new CustomMatrixError( - 'failed to read relations', - 403, - 'M_FORBIDDEN', - { - reason: "You don't have permission to access that event", - }, - ), + new CustomMatrixError('failed to read relations', 403, 'M_FORBIDDEN', { + reason: "You don't have permission to access that event", + }), ); const event: IReadRelationsFromWidgetActionRequest = { @@ -1920,18 +1896,18 @@ describe('ClientWidgetApi', () => { emitEvent(new CustomEvent('', { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3973, - ]), + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3973]), }); }); it('should handle and process the request', async () => { driver.searchUserDirectory.mockResolvedValue({ limited: true, - results: [{ - userId: '@foo:bar.com', - }], + results: [ + { + userId: '@foo:bar.com', + }, + ], }); const event: IUserDirectorySearchFromWidgetActionRequest = { @@ -1942,20 +1918,20 @@ describe('ClientWidgetApi', () => { data: { search_term: 'foo' }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { limited: true, - results: [{ - user_id: '@foo:bar.com', - display_name: undefined, - avatar_url: undefined, - }], + results: [ + { + user_id: '@foo:bar.com', + display_name: undefined, + avatar_url: undefined, + }, + ], }); }); @@ -1988,9 +1964,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2029,9 +2003,7 @@ describe('ClientWidgetApi', () => { data: { search_term: '' }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2072,9 +2044,7 @@ describe('ClientWidgetApi', () => { data: {}, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2097,9 +2067,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2111,9 +2079,7 @@ describe('ClientWidgetApi', () => { }); it('should reject requests when the driver throws an exception', async () => { - driver.searchUserDirectory.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); + driver.searchUserDirectory.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2123,9 +2089,7 @@ describe('ClientWidgetApi', () => { data: { search_term: 'foo' }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2140,15 +2104,10 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.searchUserDirectory.mockRejectedValue( - new CustomMatrixError( - 'failed to search the user directory', - 429, - 'M_LIMIT_EXCEEDED', - { - reason: 'Too many requests', - retry_after_ms: 2000, - }, - ), + new CustomMatrixError('failed to search the user directory', 429, 'M_LIMIT_EXCEEDED', { + reason: 'Too many requests', + retry_after_ms: 2000, + }), ); const event: IUserDirectorySearchFromWidgetActionRequest = { @@ -2159,9 +2118,7 @@ describe('ClientWidgetApi', () => { data: { search_term: 'foo' }, }; - await loadIframe([ - 'org.matrix.msc3973.user_directory_search', - ]); + await loadIframe(['org.matrix.msc3973.user_directory_search']); emitEvent(new CustomEvent('', { detail: event })); @@ -2199,9 +2156,7 @@ describe('ClientWidgetApi', () => { emitEvent(new CustomEvent('', { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), }); }); @@ -2218,9 +2173,7 @@ describe('ClientWidgetApi', () => { data: {}, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2252,9 +2205,7 @@ describe('ClientWidgetApi', () => { }); it('should reject requests when the driver throws an exception', async () => { - driver.getMediaConfig.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); + driver.getMediaConfig.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2264,9 +2215,7 @@ describe('ClientWidgetApi', () => { data: {}, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2281,15 +2230,10 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.getMediaConfig.mockRejectedValue( - new CustomMatrixError( - 'failed to get the media configuration', - 429, - 'M_LIMIT_EXCEEDED', - { - reason: 'Too many requests', - retry_after_ms: 2000, - }, - ), + new CustomMatrixError('failed to get the media configuration', 429, 'M_LIMIT_EXCEEDED', { + reason: 'Too many requests', + retry_after_ms: 2000, + }), ); const event: IGetMediaConfigActionFromWidgetActionRequest = { @@ -2300,9 +2244,7 @@ describe('ClientWidgetApi', () => { data: {}, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2340,9 +2282,7 @@ describe('ClientWidgetApi', () => { emitEvent(new CustomEvent('', { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), }); }); }); @@ -2363,9 +2303,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2399,9 +2337,7 @@ describe('ClientWidgetApi', () => { }); it('should reject requests when the driver throws an exception', async () => { - driver.uploadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); + driver.uploadFile.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2413,9 +2349,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2430,15 +2364,10 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.uploadFile.mockRejectedValue( - new CustomMatrixError( - 'failed to upload a file', - 429, - 'M_LIMIT_EXCEEDED', - { - reason: 'Too many requests', - retry_after_ms: 2000, - }, - ), + new CustomMatrixError('failed to upload a file', 429, 'M_LIMIT_EXCEEDED', { + reason: 'Too many requests', + retry_after_ms: 2000, + }), ); const event: IUploadFileActionFromWidgetActionRequest = { @@ -2451,9 +2380,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.upload_file', - ]); + await loadIframe(['org.matrix.msc4039.upload_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2494,9 +2421,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.download_file', - ]); + await loadIframe(['org.matrix.msc4039.download_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2506,7 +2431,7 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.downloadFile).toHaveBeenCalledWith( 'mxc://example.com/test_file'); + expect(driver.downloadFile).toHaveBeenCalledWith('mxc://example.com/test_file'); }); it('should reject requests when the capability was not requested', async () => { @@ -2530,9 +2455,7 @@ describe('ClientWidgetApi', () => { }); it('should reject requests when the driver throws an exception', async () => { - driver.downloadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); + driver.downloadFile.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2544,9 +2467,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.download_file', - ]); + await loadIframe(['org.matrix.msc4039.download_file']); emitEvent(new CustomEvent('', { detail: event })); @@ -2561,15 +2482,10 @@ describe('ClientWidgetApi', () => { driver.processError.mockImplementation(processCustomMatrixError); driver.downloadFile.mockRejectedValue( - new CustomMatrixError( - 'failed to download a file', - 429, - 'M_LIMIT_EXCEEDED', - { - reason: 'Too many requests', - retry_after_ms: 2000, - }, - ), + new CustomMatrixError('failed to download a file', 429, 'M_LIMIT_EXCEEDED', { + reason: 'Too many requests', + retry_after_ms: 2000, + }), ); const event: IDownloadFileActionFromWidgetActionRequest = { @@ -2582,9 +2498,7 @@ describe('ClientWidgetApi', () => { }, }; - await loadIframe([ - 'org.matrix.msc4039.download_file', - ]); + await loadIframe(['org.matrix.msc4039.download_file']); emitEvent(new CustomEvent('', { detail: event })); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index d95dcb8..f458fb1 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -73,7 +73,7 @@ class ClientTransportHelper { public constructor(private channels: TransportChannels) {} public trackRequest(action: WidgetApiFromWidgetAction, data: IWidgetApiRequestData): void { - this.channels.requestQueue.push({action, data}); + this.channels.requestQueue.push({ action, data }); } public nextQueuedResponse(): IWidgetApiRequestData | undefined { @@ -93,13 +93,10 @@ describe('WidgetApi', () => { clientListener = (e: MessageEvent): void => { if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response - if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request + if ('response' in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request const request = e.data; - clientTrafficHelper.trackRequest( - request.action as WidgetApiFromWidgetAction, - request.data, - ); + clientTrafficHelper.trackRequest(request.action as WidgetApiFromWidgetAction, request.data); const response = clientTrafficHelper.nextQueuedResponse(); if (response) { @@ -108,35 +105,41 @@ describe('WidgetApi', () => { ...request, response: response, } satisfies IWidgetApiResponse, - "*", + '*', ); } }; - window.addEventListener("message", clientListener); + window.addEventListener('message', clientListener); - widgetApi = new WidgetApi("WidgetApi-test", "*"); + widgetApi = new WidgetApi('WidgetApi-test', '*'); widgetApi.start(); }); afterEach(() => { - window.removeEventListener("message", clientListener); + window.removeEventListener('message', clientListener); }); describe('readEventRelations', () => { it('should forward the request to the ClientWidgetApi', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3869] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { - chunk: [], - } as IReadRelationsFromWidgetResponseData, - ); - - await expect(widgetApi.readEventRelations( - '$event', '!room-id', 'm.reference', 'm.room.message', 25, - 'from-token', 'to-token', 'f', - )).resolves.toEqual({ + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + chunk: [], + } as IReadRelationsFromWidgetResponseData); + + await expect( + widgetApi.readEventRelations( + '$event', + '!room-id', + 'm.reference', + 'm.room.message', + 25, + 'from-token', + 'to-token', + 'f', + ), + ).resolves.toEqual({ chunk: [], }); @@ -157,14 +160,20 @@ describe('WidgetApi', () => { }); it('should reject the request if the api is not supported', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [] } as ISupportedVersionsActionResponseData, - ); - - await expect(widgetApi.readEventRelations( - '$event', '!room-id', 'm.reference', 'm.room.message', 25, - 'from-token', 'to-token', 'f', - )).rejects.toThrow("The read_relations action is not supported by the client."); + widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); + + await expect( + widgetApi.readEventRelations( + '$event', + '!room-id', + 'm.reference', + 'm.room.message', + 25, + 'from-token', + 'to-token', + 'f', + ), + ).rejects.toThrow('The read_relations action is not supported by the client.'); const request = widgetTransportHelper.nextTrackedRequest(); expect(request).not.toBeUndefined(); @@ -175,100 +184,99 @@ describe('WidgetApi', () => { }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3869] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } } as IWidgetApiErrorResponseData, - ); - - await expect(widgetApi.readEventRelations( - '$event', '!room-id', 'm.reference', 'm.room.message', 25, - 'from-token', 'to-token', 'f', - )).rejects.toThrow('An error occurred'); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: 'An error occurred' }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.readEventRelations( + '$event', + '!room-id', + 'm.reference', + 'm.room.message', + 25, + 'from-token', + 'to-token', + 'f', + ), + ).rejects.toThrow('An error occurred'); }); it('should handle an error with details', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3869] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); - - await expect(widgetApi.readEventRelations( - '$event', '!room-id', 'm.reference', 'm.room.message', 25, - 'from-token', 'to-token', 'f', - )).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.readEventRelations( + '$event', + '!room-id', + 'm.reference', + 'm.room.message', + 25, + 'from-token', + 'to-token', + 'f', + ), + ).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); }); }); describe('sendEvent', () => { it('sends message events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - event_id: '$event', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + event_id: '$event', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - )).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).resolves.toEqual({ room_id: '!room-id', event_id: '$event', }); }); it('sends state events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - event_id: '$event', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + event_id: '$event', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendStateEvent( - 'm.room.topic', - "", - {}, - '!room-id', - )).resolves.toEqual({ + await expect(widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id')).resolves.toEqual({ room_id: '!room-id', event_id: '$event', }); }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { message: 'An error occurred' }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - )).rejects.toThrow('An error occurred'); + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).rejects.toThrow( + 'An error occurred', + ); }); it('should handle an error with details', async () => { @@ -276,124 +284,86 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - )).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).rejects.toThrow( + new WidgetApiResponseError('An error occurred', errorDetails), + ); }); }); describe('delayed sendEvent', () => { it('sends delayed message events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - delay_id: 'id', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + delay_id: 'id', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - 2000, - )).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 2000)).resolves.toEqual({ room_id: '!room-id', delay_id: 'id', }); }); it('sends delayed state events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - delay_id: 'id', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + delay_id: 'id', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendStateEvent( - 'm.room.topic', - "", - {}, - '!room-id', - 2000, - )).resolves.toEqual({ + await expect(widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id', 2000)).resolves.toEqual({ room_id: '!room-id', delay_id: 'id', }); }); it('sends delayed child action message events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - delay_id: 'id', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + delay_id: 'id', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - 1000, - undefined, - )).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).resolves.toEqual({ room_id: '!room-id', delay_id: 'id', }); }); it('sends delayed child action state events', async () => { - widgetTransportHelper.queueResponse( - { - room_id: '!room-id', - delay_id: 'id', - } as ISendEventFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + room_id: '!room-id', + delay_id: 'id', + } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendStateEvent( - 'm.room.topic', - "", - {}, - '!room-id', - 1000, - undefined, - )).resolves.toEqual({ + await expect( + widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id', 1000, undefined), + ).resolves.toEqual({ room_id: '!room-id', delay_id: 'id', }); }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { message: 'An error occurred' }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - 1000, - undefined, - )).rejects.toThrow('An error occurred'); + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).rejects.toThrow( + 'An error occurred', + ); }); it('should handle an error with details', async () => { @@ -401,51 +371,41 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent( - 'm.room.message', - {}, - '!room-id', - 1000, - undefined, - )).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).rejects.toThrow( + new WidgetApiResponseError('An error occurred', errorDetails), + ); }); }); describe('updateDelayedEvent', () => { it('updates delayed events', async () => { widgetTransportHelper.queueResponse({}); - await expect(widgetApi.updateDelayedEvent( - 'id', - UpdateDelayedEventAction.Send, - )).resolves.toEqual({}); + await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).resolves.toEqual({}); }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { message: 'An error occurred' }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.updateDelayedEvent( - 'id', - UpdateDelayedEventAction.Send, - )).rejects.toThrow('An error occurred'); + await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).rejects.toThrow( + 'An error occurred', + ); }); it('should handle an error with details', async () => { @@ -453,55 +413,42 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.updateDelayedEvent( - 'id', - UpdateDelayedEventAction.Send, - )).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).rejects.toThrow( + new WidgetApiResponseError('An error occurred', errorDetails), + ); }); }); describe('getClientVersions', () => { beforeEach(() => { - widgetTransportHelper.queueResponse( - { - supported_versions: [ - UnstableApiVersion.MSC3869, UnstableApiVersion.MSC2762, - ], - } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869, UnstableApiVersion.MSC2762], + } as ISupportedVersionsActionResponseData); }); it('should request supported client versions', async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - 'org.matrix.msc3869', 'org.matrix.msc2762', - ]); + await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); }); it('should cache supported client versions on successive calls', async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - 'org.matrix.msc3869', 'org.matrix.msc2762', - ]); + await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - 'org.matrix.msc3869', 'org.matrix.msc2762', - ]); + await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); @@ -510,19 +457,15 @@ describe('WidgetApi', () => { describe('searchUserDirectory', () => { it('should forward the request to the ClientWidgetApi', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3973] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { - limited: false, - results: [], - } as IUserDirectorySearchFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + limited: false, + results: [], + } as IUserDirectorySearchFromWidgetResponseData); - await expect(widgetApi.searchUserDirectory( - 'foo', 10, - )).resolves.toEqual({ + await expect(widgetApi.searchUserDirectory('foo', 10)).resolves.toEqual({ limited: false, results: [], }); @@ -538,13 +481,11 @@ describe('WidgetApi', () => { }); it('should reject the request if the api is not supported', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.searchUserDirectory( - 'foo', 10, - )).rejects.toThrow("The user_directory_search action is not supported by the client."); + await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow( + 'The user_directory_search action is not supported by the client.', + ); const request = widgetTransportHelper.nextTrackedRequest(); expect(request).not.toBeUndefined(); @@ -555,58 +496,52 @@ describe('WidgetApi', () => { }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3973] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } }, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); - await expect(widgetApi.searchUserDirectory( - 'foo', 10, - )).rejects.toThrow('An error occurred'); + await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow('An error occurred'); }); it('should handle an error with details', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC3973] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.searchUserDirectory( - 'foo', 10, - )).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow( + new WidgetApiResponseError('An error occurred', errorDetails), + ); }); }); describe('getMediaConfig', () => { it('should forward the request to the ClientWidgetApi', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { 'm.upload.size': 1000 } as IGetMediaConfigActionFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + 'm.upload.size': 1000, + } as IGetMediaConfigActionFromWidgetResponseData); await expect(widgetApi.getMediaConfig()).resolves.toEqual({ 'm.upload.size': 1000, @@ -620,12 +555,10 @@ describe('WidgetApi', () => { }); it('should reject the request if the api is not supported', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( - "The get_media_config action is not supported by the client.", + 'The get_media_config action is not supported by the client.', ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -637,43 +570,37 @@ describe('WidgetApi', () => { }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } }, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - 'An error occurred', - ); + await expect(widgetApi.getMediaConfig()).rejects.toThrow('An error occurred'); }); it('should handle an error with details', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( new WidgetApiResponseError('An error occurred', errorDetails), @@ -683,31 +610,29 @@ describe('WidgetApi', () => { describe('uploadFile', () => { it('should forward the request to the ClientWidgetApi', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { content_uri: 'mxc://...' } as IUploadFileActionFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + content_uri: 'mxc://...', + } as IUploadFileActionFromWidgetResponseData); - await expect(widgetApi.uploadFile("data")).resolves.toEqual({ + await expect(widgetApi.uploadFile('data')).resolves.toEqual({ content_uri: 'mxc://...', }); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { file: "data" }, + data: { file: 'data' }, } satisfies SendRequestArgs); }); it('should reject the request if the api is not supported', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - "The upload_file action is not supported by the client.", + await expect(widgetApi.uploadFile('data')).rejects.toThrow( + 'The upload_file action is not supported by the client.', ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -719,45 +644,39 @@ describe('WidgetApi', () => { }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } }, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - 'An error occurred', - ); + await expect(widgetApi.uploadFile('data')).rejects.toThrow('An error occurred'); }); it('should handle an error with details', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.uploadFile("data")).rejects.toThrow( + await expect(widgetApi.uploadFile('data')).rejects.toThrow( new WidgetApiResponseError('An error occurred', errorDetails), ); }); @@ -765,31 +684,27 @@ describe('WidgetApi', () => { describe('downloadFile', () => { it('should forward the request to the ClientWidgetApi', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { file: 'test contents' } as IDownloadFileActionFromWidgetResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ file: 'test contents' } as IDownloadFileActionFromWidgetResponseData); - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).resolves.toEqual({ + await expect(widgetApi.downloadFile('mxc://example.com/test_file')).resolves.toEqual({ file: 'test contents', }); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { content_uri: "mxc://example.com/test_file" }, + data: { content_uri: 'mxc://example.com/test_file' }, } satisfies SendRequestArgs); }); it('should reject the request if the api is not supported', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( - "The download_file action is not supported by the client.", + await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow( + 'The download_file action is not supported by the client.', ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -801,45 +716,39 @@ describe('WidgetApi', () => { }); it('should handle an error', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); - widgetTransportHelper.queueResponse( - { error: { message: 'An error occurred' } }, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( - 'An error occurred', - ); + await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow('An error occurred'); }); it('should handle an error with details', async () => { - widgetTransportHelper.queueResponse( - { supported_versions: [UnstableApiVersion.MSC4039] } as ISupportedVersionsActionResponseData, - ); + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: "", + url: '', response: { - errcode: "M_UNKNOWN", + errcode: 'M_UNKNOWN', error: 'Unknown error', }, }, }; - widgetTransportHelper.queueResponse( - { - error: { - message: 'An error occurred', - ...errorDetails, - }, - } as IWidgetApiErrorResponseData, - ); + widgetTransportHelper.queueResponse({ + error: { + message: 'An error occurred', + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( + await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow( new WidgetApiResponseError('An error occurred', errorDetails), ); }); diff --git a/test/url-template-test.ts b/test/url-template-test.ts index b1db0fe..cb4bafa 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -14,34 +14,34 @@ * limitations under the License. */ -import { runTemplate } from "../src"; +import { runTemplate } from '../src'; -describe("runTemplate", () => { - it("should replace device id template in url", () => { - const url = "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; +describe('runTemplate', () => { + it('should replace device id template in url', () => { + const url = 'https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id'; const replacedUrl = runTemplate( url, { - id: "widget-id", + id: 'widget-id', creatorUserId: '@user-id', type: 'type', url, }, { - deviceId: "my-device-id", + deviceId: 'my-device-id', currentUserId: '@user-id', }, ); - expect(replacedUrl).toBe("https://localhost/?my-query#device_id=my-device-id"); + expect(replacedUrl).toBe('https://localhost/?my-query#device_id=my-device-id'); }); - it("should replace base url template in url", () => { - const url = "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; + it('should replace base url template in url', () => { + const url = 'https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url'; const replacedUrl = runTemplate( url, { - id: "widget-id", + id: 'widget-id', creatorUserId: '@user-id', type: 'type', url, @@ -52,6 +52,6 @@ describe("runTemplate", () => { }, ); - expect(replacedUrl).toBe("https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi"); + expect(replacedUrl).toBe('https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi'); }); }); diff --git a/tsconfig-dev.json b/tsconfig-dev.json index 4ca2a2a..5ef424e 100644 --- a/tsconfig-dev.json +++ b/tsconfig-dev.json @@ -1,6 +1,4 @@ { "extends": "./tsconfig.json", - "include": [ - "./test/**/*.ts" - ] + "include": ["./test/**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 5abd7b9..f58ceb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,13 +10,8 @@ "outDir": "./lib", "declaration": true, "types": ["jest"], - "lib": [ - "es2020", - "dom" - ], + "lib": ["es2020", "dom"], "strict": true }, - "include": [ - "./src/**/*.ts" - ] + "include": ["./src/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index 2ac0950..6c49d1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5849,6 +5849,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +prettier@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== + pretty-format@^27.0.2: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" From bb93831571330231e276804c772358dc2509b186 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:19:49 +0100 Subject: [PATCH 04/11] use double quotes in tests as well. --- package.json | 8 - test/ClientWidgetApi-test.ts | 1338 +++++++++++++++++----------------- test/WidgetApi-test.ts | 430 +++++------ test/url-template-test.ts | 36 +- 4 files changed, 902 insertions(+), 910 deletions(-) diff --git a/package.json b/package.json index 4851ef9..3e4a189 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,6 @@ "options": { "tabWidth": 4 } - }, - { - "files": [ - "test/**/*.ts" - ], - "options": { - "singleQuote": true - } } ] }, diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 15bec9e..0a261f2 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -15,21 +15,21 @@ * limitations under the License. */ -import { waitFor } from '@testing-library/dom'; - -import { ClientWidgetApi } from '../src/ClientWidgetApi'; -import { WidgetDriver } from '../src/driver/WidgetDriver'; -import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; -import { Capability } from '../src/interfaces/Capabilities'; -import { IRoomEvent } from '../src/interfaces/IRoomEvent'; -import { IWidgetApiRequest } from '../src/interfaces/IWidgetApiRequest'; -import { IReadRelationsFromWidgetActionRequest } from '../src/interfaces/ReadRelationsAction'; -import { ISupportedVersionsActionRequest } from '../src/interfaces/SupportedVersionsAction'; -import { IUserDirectorySearchFromWidgetActionRequest } from '../src/interfaces/UserDirectorySearchAction'; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from '../src/interfaces/WidgetApiAction'; -import { WidgetApiDirection } from '../src/interfaces/WidgetApiDirection'; -import { Widget } from '../src/models/Widget'; -import { PostmessageTransport } from '../src/transport/PostmessageTransport'; +import { waitFor } from "@testing-library/dom"; + +import { ClientWidgetApi } from "../src/ClientWidgetApi"; +import { WidgetDriver } from "../src/driver/WidgetDriver"; +import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { Capability } from "../src/interfaces/Capabilities"; +import { IRoomEvent } from "../src/interfaces/IRoomEvent"; +import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; +import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; +import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; +import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; +import { Widget } from "../src/models/Widget"; +import { PostmessageTransport } from "../src/transport/PostmessageTransport"; import { IDownloadFileActionFromWidgetActionRequest, IGetOpenIDActionRequest, @@ -45,11 +45,11 @@ import { SimpleObservable, Symbols, UpdateDelayedEventAction, -} from '../src'; -import { IGetMediaConfigActionFromWidgetActionRequest } from '../src/interfaces/GetMediaConfigAction'; -import { IReadRoomAccountDataFromWidgetActionRequest } from '../src/interfaces/ReadRoomAccountDataAction'; +} from "../src"; +import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction"; +import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction"; -jest.mock('../src/transport/PostmessageTransport'); +jest.mock("../src/transport/PostmessageTransport"); afterEach(() => { jest.resetAllMocks(); @@ -57,12 +57,12 @@ afterEach(() => { function createRoomEvent(event: Partial = {}): IRoomEvent { return { - type: 'm.room.message', - sender: 'user-id', + type: "m.room.message", + sender: "user-id", content: {}, origin_server_ts: 0, - event_id: 'id-0', - room_id: '!room-id', + event_id: "id-0", + room_id: "!room-id", unsigned: {}, ...event, }; @@ -85,7 +85,7 @@ function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetail matrix_api_error: { http_status: e.httpStatus, http_headers: {}, - url: '', + url: "", response: { errcode: e.name, error: e.message, @@ -96,29 +96,29 @@ function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetail : undefined; } -describe('ClientWidgetApi', () => { +describe("ClientWidgetApi", () => { let capabilities: Capability[]; let iframe: HTMLIFrameElement; let driver: jest.Mocked; let clientWidgetApi: ClientWidgetApi; let transport: PostmessageTransport; - let emitEvent: Parameters['1']; + let emitEvent: Parameters["1"]; async function loadIframe(caps: Capability[] = []): Promise { capabilities = caps; const ready = new Promise((resolve) => { - clientWidgetApi.once('ready', resolve); + clientWidgetApi.once("ready", resolve); }); - iframe.dispatchEvent(new Event('load')); + iframe.dispatchEvent(new Event("load")); await ready; } beforeEach(() => { capabilities = []; - iframe = document.createElement('iframe'); + iframe = document.createElement("iframe"); document.body.appendChild(iframe); driver = { @@ -143,10 +143,10 @@ describe('ClientWidgetApi', () => { clientWidgetApi = new ClientWidgetApi( new Widget({ - id: 'test', - creatorUserId: '@alice:example.org', - type: 'example', - url: 'https://example.org', + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org", }), iframe, driver, @@ -164,30 +164,30 @@ describe('ClientWidgetApi', () => { iframe.remove(); }); - it('should initiate capabilities', async () => { - await loadIframe(['m.always_on_screen']); + it("should initiate capabilities", async () => { + await loadIframe(["m.always_on_screen"]); - expect(clientWidgetApi.hasCapability('m.always_on_screen')).toBe(true); - expect(clientWidgetApi.hasCapability('m.sticker')).toBe(false); + expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true); + expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false); }); - describe('navigate action', () => { - it('navigates', async () => { + describe("navigate action", () => { + it("navigates", async () => { driver.navigate.mockResolvedValue(Promise.resolve()); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2931Navigate, data: { - uri: 'https://matrix.to/#/#room:example.net', + uri: "https://matrix.to/#/#room:example.net", }, }; - await loadIframe(['org.matrix.msc2931.navigate']); + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, {}); @@ -196,113 +196,113 @@ describe('ClientWidgetApi', () => { expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); }); - it('fails to navigate', async () => { + it("fails to navigate", async () => { const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2931Navigate, data: { - uri: 'https://matrix.to/#/#room:example.net', + uri: "https://matrix.to/#/#room:example.net", }, }; await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Missing capability' }, + error: { message: "Missing capability" }, }); }); expect(driver.navigate).not.toBeCalled(); }); - it('fails to navigate to an unsupported URI', async () => { + it("fails to navigate to an unsupported URI", async () => { const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2931Navigate, data: { - uri: 'https://example.net', + uri: "https://example.net", }, }; - await loadIframe(['org.matrix.msc2931.navigate']); + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid matrix.to URI' }, + error: { message: "Invalid matrix.to URI" }, }); }); expect(driver.navigate).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { - driver.navigate.mockRejectedValue(new Error('M_UNKNOWN: Unknown error')); + it("should reject requests when the driver throws an exception", async () => { + driver.navigate.mockRejectedValue(new Error("M_UNKNOWN: Unknown error")); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2931Navigate, data: { - uri: 'https://matrix.to/#/#room:example.net', + uri: "https://matrix.to/#/#room:example.net", }, }; - await loadIframe(['org.matrix.msc2931.navigate']); + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Error handling navigation' }, + error: { message: "Error handling navigation" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.navigate.mockRejectedValue( - new CustomMatrixError('failed to navigate', 400, 'M_UNKNOWN', { - reason: 'Unknown error', + new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { + reason: "Unknown error", }), ); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2931Navigate, data: { - uri: 'https://matrix.to/#/#room:example.net', + uri: "https://matrix.to/#/#room:example.net", }, }; - await loadIframe(['org.matrix.msc2931.navigate']); + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Error handling navigation', + message: "Error handling navigation", matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'failed to navigate', - reason: 'Unknown error', + errcode: "M_UNKNOWN", + error: "failed to navigate", + reason: "Unknown error", }, } satisfies IMatrixApiError, }, @@ -311,10 +311,10 @@ describe('ClientWidgetApi', () => { }); }); - describe('send_event action', () => { - it('sends message events', async () => { - const roomId = '!room:example.org'; - const eventId = '$event:example.org'; + describe("send_event action", () => { + it("sends message events", async () => { + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; driver.sendEvent.mockResolvedValue({ roomId, @@ -323,11 +323,11 @@ describe('ClientWidgetApi', () => { const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', + type: "m.room.message", content: {}, room_id: roomId, }, @@ -338,7 +338,7 @@ describe('ClientWidgetApi', () => { `org.matrix.msc2762.send.event:${event.data.type}`, ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -350,9 +350,9 @@ describe('ClientWidgetApi', () => { expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, null, roomId); }); - it('sends state events', async () => { - const roomId = '!room:example.org'; - const eventId = '$event:example.org'; + it("sends state events", async () => { + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; driver.sendEvent.mockResolvedValue({ roomId, @@ -361,13 +361,13 @@ describe('ClientWidgetApi', () => { const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.topic', + type: "m.room.topic", content: {}, - state_key: '', + state_key: "", room_id: roomId, }, }; @@ -377,7 +377,7 @@ describe('ClientWidgetApi', () => { `org.matrix.msc2762.send.state_event:${event.data.type}`, ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -386,22 +386,22 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, '', roomId); + expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, "", roomId); }); - it('should reject requests when the driver throws an exception', async () => { - const roomId = '!room:example.org'; + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org"; - driver.sendEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); + driver.sendEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', - content: 'hello', + type: "m.room.message", + content: "hello", room_id: roomId, }, }; @@ -411,34 +411,34 @@ describe('ClientWidgetApi', () => { `org.matrix.msc2762.send.event:${event.data.type}`, ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Error sending event' }, + error: { message: "Error sending event" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { - const roomId = '!room:example.org'; + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org"; driver.processError.mockImplementation(processCustomMatrixError); driver.sendEvent.mockRejectedValue( - new CustomMatrixError('failed to send event', 400, 'M_NOT_JSON', { - reason: 'Content must be a JSON object.', + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", }), ); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', - content: 'hello', + type: "m.room.message", + content: "hello", room_id: roomId, }, }; @@ -448,20 +448,20 @@ describe('ClientWidgetApi', () => { `org.matrix.msc2762.send.event:${event.data.type}`, ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Error sending event', + message: "Error sending event", matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_NOT_JSON', - error: 'failed to send event', - reason: 'Content must be a JSON object.', + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", }, } satisfies IMatrixApiError, }, @@ -470,17 +470,17 @@ describe('ClientWidgetApi', () => { }); }); - describe('send_event action for delayed events', () => { - it('fails to send delayed events', async () => { - const roomId = '!room:example.org'; + describe("send_event action for delayed events", () => { + it("fails to send delayed events", async () => { + const roomId = "!room:example.org"; const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', + type: "m.room.message", content: {}, delay: 5000, room_id: roomId, @@ -493,7 +493,7 @@ describe('ClientWidgetApi', () => { // Without the required capability ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -504,10 +504,10 @@ describe('ClientWidgetApi', () => { expect(driver.sendDelayedEvent).not.toBeCalled(); }); - it('sends delayed message events', async () => { - const roomId = '!room:example.org'; - const parentDelayId = 'fp'; - const timeoutDelayId = 'ft'; + it("sends delayed message events", async () => { + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ roomId, @@ -516,11 +516,11 @@ describe('ClientWidgetApi', () => { const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', + type: "m.room.message", content: {}, room_id: roomId, delay: 5000, @@ -531,10 +531,10 @@ describe('ClientWidgetApi', () => { await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - 'org.matrix.msc4157.send.delayed_event', + "org.matrix.msc4157.send.delayed_event", ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -553,10 +553,10 @@ describe('ClientWidgetApi', () => { ); }); - it('sends delayed state events', async () => { - const roomId = '!room:example.org'; - const parentDelayId = 'fp'; - const timeoutDelayId = 'ft'; + it("sends delayed state events", async () => { + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ roomId, @@ -565,13 +565,13 @@ describe('ClientWidgetApi', () => { const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.topic', + type: "m.room.topic", content: {}, - state_key: '', + state_key: "", room_id: roomId, delay: 5000, parent_delay_id: parentDelayId, @@ -581,10 +581,10 @@ describe('ClientWidgetApi', () => { await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.state_event:${event.data.type}`, - 'org.matrix.msc4157.send.delayed_event', + "org.matrix.msc4157.send.delayed_event", ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -598,90 +598,90 @@ describe('ClientWidgetApi', () => { event.data.parent_delay_id, event.data.type, event.data.content, - '', + "", roomId, ); }); - it('should reject requests when the driver throws an exception', async () => { - const roomId = '!room:example.org'; + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org"; - driver.sendDelayedEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); + driver.sendDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', - content: 'hello', + type: "m.room.message", + content: "hello", room_id: roomId, delay: 5000, - parent_delay_id: 'fp', + parent_delay_id: "fp", }, }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - 'org.matrix.msc4157.send.delayed_event', + "org.matrix.msc4157.send.delayed_event", ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Error sending event' }, + error: { message: "Error sending event" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { - const roomId = '!room:example.org'; + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org"; driver.processError.mockImplementation(processCustomMatrixError); driver.sendDelayedEvent.mockRejectedValue( - new CustomMatrixError('failed to send event', 400, 'M_NOT_JSON', { - reason: 'Content must be a JSON object.', + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", }), ); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendEvent, data: { - type: 'm.room.message', - content: 'hello', + type: "m.room.message", + content: "hello", room_id: roomId, delay: 5000, - parent_delay_id: 'fp', + parent_delay_id: "fp", }, }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - 'org.matrix.msc4157.send.delayed_event', + "org.matrix.msc4157.send.delayed_event", ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Error sending event', + message: "Error sending event", matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_NOT_JSON', - error: 'failed to send event', - reason: 'Content must be a JSON object.', + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", }, } satisfies IMatrixApiError, }, @@ -690,21 +690,21 @@ describe('ClientWidgetApi', () => { }); }); - describe('receiving events', () => { - const roomId = '!room:example.org'; - const otherRoomId = '!other-room:example.org'; - const event = createRoomEvent({ room_id: roomId, type: 'm.room.message', content: 'hello' }); + describe("receiving events", () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + const event = createRoomEvent({ room_id: roomId, type: "m.room.message", content: "hello" }); const eventFromOtherRoom = createRoomEvent({ room_id: otherRoomId, - type: 'm.room.message', - content: 'test', + type: "m.room.message", + content: "test", }); - it('forwards events to the widget from one room only', async () => { + it("forwards events to the widget from one room only", async () => { // Give the widget capabilities to receive from just one room await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, - 'org.matrix.msc2762.receive.event:m.room.message', + "org.matrix.msc2762.receive.event:m.room.message", ]); // Event from the matching room should be forwarded @@ -716,13 +716,13 @@ describe('ClientWidgetApi', () => { expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); }); - it('forwards events to the widget from the currently viewed room', async () => { + it("forwards events to the widget from the currently viewed room", async () => { clientWidgetApi.setViewedRoomId(roomId); // Give the widget capabilities to receive events without specifying // any rooms that it can read await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, - 'org.matrix.msc2762.receive.event:m.room.message', + "org.matrix.msc2762.receive.event:m.room.message", ]); // Event from the viewed room should be forwarded @@ -739,11 +739,11 @@ describe('ClientWidgetApi', () => { expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); }); - it('forwards events to the widget from all rooms', async () => { + it("forwards events to the widget from all rooms", async () => { // Give the widget capabilities to receive from any known room await loadIframe([ `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - 'org.matrix.msc2762.receive.event:m.room.message', + "org.matrix.msc2762.receive.event:m.room.message", ]); // Events from both rooms should be forwarded @@ -754,34 +754,34 @@ describe('ClientWidgetApi', () => { }); }); - describe('receiving room state', () => { - it('syncs initial state and feeds updates', async () => { - const roomId = '!room:example.org'; - const otherRoomId = '!other-room:example.org'; + describe("receiving room state", () => { + it("syncs initial state and feeds updates", async () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; clientWidgetApi.setViewedRoomId(roomId); const topicEvent = createRoomEvent({ room_id: roomId, - type: 'm.room.topic', - state_key: '', - content: { topic: 'Hello world!' }, + type: "m.room.topic", + state_key: "", + content: { topic: "Hello world!" }, }); const nameEvent = createRoomEvent({ room_id: roomId, - type: 'm.room.name', - state_key: '', - content: { name: 'Test room' }, + type: "m.room.name", + state_key: "", + content: { name: "Test room" }, }); const joinRulesEvent = createRoomEvent({ room_id: roomId, - type: 'm.room.join_rules', - state_key: '', - content: { join_rule: 'public' }, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "public" }, }); const otherRoomNameEvent = createRoomEvent({ room_id: otherRoomId, - type: 'm.room.name', - state_key: '', - content: { name: 'Other room' }, + type: "m.room.name", + state_key: "", + content: { name: "Other room" }, }); // Artificially delay the delivery of the join rules event @@ -790,31 +790,31 @@ describe('ClientWidgetApi', () => { driver.readRoomState.mockImplementation(async (rId, eventType, stateKey) => { if (rId === roomId) { - if (eventType === 'm.room.topic' && stateKey === '') return [topicEvent]; - if (eventType === 'm.room.name' && stateKey === '') return [nameEvent]; - if (eventType === 'm.room.join_rules' && stateKey === '') { + if (eventType === "m.room.topic" && stateKey === "") return [topicEvent]; + if (eventType === "m.room.name" && stateKey === "") return [nameEvent]; + if (eventType === "m.room.join_rules" && stateKey === "") { await joinRules; return [joinRulesEvent]; } } else if (rId === otherRoomId) { - if (eventType === 'm.room.name' && stateKey === '') return [otherRoomNameEvent]; + if (eventType === "m.room.name" && stateKey === "") return [otherRoomNameEvent]; } return []; }); await loadIframe([ - 'org.matrix.msc2762.receive.state_event:m.room.topic#', - 'org.matrix.msc2762.receive.state_event:m.room.name#', - 'org.matrix.msc2762.receive.state_event:m.room.join_rules#', + "org.matrix.msc2762.receive.state_event:m.room.topic#", + "org.matrix.msc2762.receive.state_event:m.room.name#", + "org.matrix.msc2762.receive.state_event:m.room.join_rules#", ]); // Simulate a race between reading the original join rules event and // the join rules being updated at the same time const newJoinRulesEvent = createRoomEvent({ room_id: roomId, - type: 'm.room.join_rules', - state_key: '', - content: { join_rule: 'invite' }, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "invite" }, }); clientWidgetApi.feedStateUpdate(newJoinRulesEvent); // What happens if the original join rules are delivered after the @@ -836,9 +836,9 @@ describe('ClientWidgetApi', () => { // as expected const newTopicEvent = createRoomEvent({ room_id: roomId, - type: 'm.room.topic', - state_key: '', - content: { topic: 'Our new topic' }, + type: "m.room.topic", + state_key: "", + content: { topic: "Our new topic" }, }); clientWidgetApi.feedStateUpdate(newTopicEvent); @@ -866,22 +866,22 @@ describe('ClientWidgetApi', () => { }); }); - describe('update_delayed_event action', () => { - it('fails to update delayed events', async () => { + describe("update_delayed_event action", () => { + it("fails to update delayed events", async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, data: { - delay_id: 'f', + delay_id: "f", action: UpdateDelayedEventAction.Send, }, }; await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -892,21 +892,21 @@ describe('ClientWidgetApi', () => { expect(driver.updateDelayedEvent).not.toBeCalled(); }); - it('fails to update delayed events with unsupported action', async () => { + it("fails to update delayed events with unsupported action", async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, data: { - delay_id: 'f', - action: 'unknown' as UpdateDelayedEventAction, + delay_id: "f", + action: "unknown" as UpdateDelayedEventAction, }, }; - await loadIframe(['org.matrix.msc4157.update_delayed_event']); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -917,7 +917,7 @@ describe('ClientWidgetApi', () => { expect(driver.updateDelayedEvent).not.toBeCalled(); }); - it('updates delayed events', async () => { + it("updates delayed events", async () => { driver.updateDelayedEvent.mockResolvedValue(undefined); for (const action of [ @@ -927,18 +927,18 @@ describe('ClientWidgetApi', () => { ]) { const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, data: { - delay_id: 'f', + delay_id: "f", action, }, }; - await loadIframe(['org.matrix.msc4157.update_delayed_event']); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, {}); @@ -948,67 +948,67 @@ describe('ClientWidgetApi', () => { } }); - it('should reject requests when the driver throws an exception', async () => { - driver.updateDelayedEvent.mockRejectedValue(new Error('M_BAD_JSON: Content must be a JSON object')); + it("should reject requests when the driver throws an exception", async () => { + driver.updateDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, data: { - delay_id: 'f', + delay_id: "f", action: UpdateDelayedEventAction.Send, }, }; - await loadIframe(['org.matrix.msc4157.update_delayed_event']); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Error updating delayed event' }, + error: { message: "Error updating delayed event" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.updateDelayedEvent.mockRejectedValue( - new CustomMatrixError('failed to update delayed event', 400, 'M_NOT_JSON', { - reason: 'Content must be a JSON object.', + new CustomMatrixError("failed to update delayed event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", }), ); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, data: { - delay_id: 'f', + delay_id: "f", action: UpdateDelayedEventAction.Send, }, }; - await loadIframe(['org.matrix.msc4157.update_delayed_event']); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Error updating delayed event', + message: "Error updating delayed event", matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_NOT_JSON', - error: 'failed to update delayed event', - reason: 'Content must be a JSON object.', + errcode: "M_NOT_JSON", + error: "failed to update delayed event", + reason: "Content must be a JSON object.", }, } satisfies IMatrixApiError, }, @@ -1017,20 +1017,20 @@ describe('ClientWidgetApi', () => { }); }); - describe('send_to_device action', () => { - it('sends unencrypted to-device events', async () => { + describe("send_to_device action", () => { + it("sends unencrypted to-device events", async () => { const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", encrypted: false, messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1039,7 +1039,7 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, {}); @@ -1052,18 +1052,18 @@ describe('ClientWidgetApi', () => { ); }); - it('fails to send to-device events without event type', async () => { + it("fails to send to-device events without event type", async () => { const event: IWidgetApiRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { encrypted: false, messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1072,54 +1072,54 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - missing event type' }, + error: { message: "Invalid request - missing event type" }, }); }); expect(driver.sendToDevice).not.toBeCalled(); }); - it('fails to send to-device events without event contents', async () => { + it("fails to send to-device events without event contents", async () => { const event: IWidgetApiRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", encrypted: false, }, }; await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - missing event contents' }, + error: { message: "Invalid request - missing event contents" }, }); }); expect(driver.sendToDevice).not.toBeCalled(); }); - it('fails to send to-device events without encryption flag', async () => { + it("fails to send to-device events without encryption flag", async () => { const event: IWidgetApiRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1128,30 +1128,30 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - missing encryption flag' }, + error: { message: "Invalid request - missing encryption flag" }, }); }); expect(driver.sendToDevice).not.toBeCalled(); }); - it('fails to send to-device events with any event type', async () => { + it("fails to send to-device events with any event type", async () => { const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", encrypted: false, messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1160,34 +1160,34 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}_different`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Cannot send to-device events of this type' }, + error: { message: "Cannot send to-device events of this type" }, }); }); expect(driver.sendToDevice).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { + it("should reject requests when the driver throws an exception", async () => { driver.sendToDevice.mockRejectedValue( new Error("M_FORBIDDEN: You don't have permission to send to-device events"), ); const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", encrypted: false, messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1196,36 +1196,36 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Error sending event' }, + error: { message: "Error sending event" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.sendToDevice.mockRejectedValue( - new CustomMatrixError('failed to send event', 400, 'M_FORBIDDEN', { + new CustomMatrixError("failed to send event", 400, "M_FORBIDDEN", { reason: "You don't have permission to send to-device events", }), ); const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SendToDevice, data: { - type: 'net.example.test', + type: "net.example.test", encrypted: false, messages: { - '@foo:bar.com': { + "@foo:bar.com": { DEVICEID: { - example_content_key: 'value', + example_content_key: "value", }, }, }, @@ -1234,19 +1234,19 @@ describe('ClientWidgetApi', () => { await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Error sending event', + message: "Error sending event", matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_FORBIDDEN', - error: 'failed to send event', + errcode: "M_FORBIDDEN", + error: "failed to send event", reason: "You don't have permission to send to-device events", }, } satisfies IMatrixApiError, @@ -1256,40 +1256,40 @@ describe('ClientWidgetApi', () => { }); }); - describe('get_openid action', () => { - it('gets info', async () => { + describe("get_openid action", () => { + it("gets info", async () => { driver.askOpenID.mockImplementation((observable) => { observable.update({ state: OpenIDRequestState.Allowed, token: { - access_token: 'access_token', + access_token: "access_token", }, }); }); const event: IGetOpenIDActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.GetOpenIDCredentials, data: {}, }; await loadIframe([]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { state: OpenIDRequestState.Allowed, - access_token: 'access_token', + access_token: "access_token", }); }); expect(driver.askOpenID).toHaveBeenCalledWith(expect.any(SimpleObservable)); }); - it('fails when client provided invalid token', async () => { + it("fails when client provided invalid token", async () => { driver.askOpenID.mockImplementation((observable) => { observable.update({ state: OpenIDRequestState.Allowed, @@ -1298,19 +1298,19 @@ describe('ClientWidgetApi', () => { const event: IGetOpenIDActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.GetOpenIDCredentials, data: {}, }; await loadIframe([]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { - error: { message: 'client provided invalid OIDC token for an allowed request' }, + error: { message: "client provided invalid OIDC token for an allowed request" }, }); }); @@ -1318,10 +1318,10 @@ describe('ClientWidgetApi', () => { }); }); - describe('com.beeper.read_room_account_data action', () => { - it('reads room account data', async () => { - const type = 'net.example.test'; - const roomId = '!room:example.org'; + describe("com.beeper.read_room_account_data action", () => { + it("reads room account data", async () => { + const type = "net.example.test"; + const roomId = "!room:example.org"; driver.readRoomAccountData.mockResolvedValue([ { @@ -1333,8 +1333,8 @@ describe('ClientWidgetApi', () => { const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data: { room_ids: [roomId], @@ -1344,7 +1344,7 @@ describe('ClientWidgetApi', () => { await loadIframe([`com.beeper.capabilities.receive.room_account_data:${type}`]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -1361,9 +1361,9 @@ describe('ClientWidgetApi', () => { expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); }); - it('does not read room account data', async () => { - const type = 'net.example.test'; - const roomId = '!room:example.org'; + it("does not read room account data", async () => { + const type = "net.example.test"; + const roomId = "!room:example.org"; driver.readRoomAccountData.mockResolvedValue([ { @@ -1375,8 +1375,8 @@ describe('ClientWidgetApi', () => { const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data: { room_ids: [roomId], @@ -1386,11 +1386,11 @@ describe('ClientWidgetApi', () => { await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { - error: { message: 'Cannot read room account data of this type' }, + error: { message: "Cannot read room account data of this type" }, }); }); @@ -1398,10 +1398,10 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc2876.read_events action', () => { - it('reads events from a specific room', async () => { - const roomId = '!room:example.org'; - const event = createRoomEvent({ room_id: roomId, type: 'net.example.test', content: 'test' }); + describe("org.matrix.msc2876.read_events action", () => { + it("reads events from a specific room", async () => { + const roomId = "!room:example.org"; + const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test" }); driver.readRoomTimeline.mockImplementation(async (rId) => { if (rId === roomId) return [event]; return []; @@ -1409,22 +1409,22 @@ describe('ClientWidgetApi', () => { const request: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', + type: "net.example.test", room_ids: [roomId], }, }; await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, - 'org.matrix.msc2762.receive.event:net.example.test', + "org.matrix.msc2762.receive.event:net.example.test", ]); clientWidgetApi.setViewedRoomId(roomId); - emitEvent(new CustomEvent('', { detail: request })); + emitEvent(new CustomEvent("", { detail: request })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(request, { @@ -1434,7 +1434,7 @@ describe('ClientWidgetApi', () => { expect(driver.readRoomTimeline).toHaveBeenCalledWith( roomId, - 'net.example.test', + "net.example.test", undefined, undefined, 0, @@ -1442,11 +1442,11 @@ describe('ClientWidgetApi', () => { ); }); - it('reads events from all rooms', async () => { - const roomId = '!room:example.org'; - const otherRoomId = '!other-room:example.org'; - const event = createRoomEvent({ room_id: roomId, type: 'net.example.test', content: 'test' }); - const otherRoomEvent = createRoomEvent({ room_id: otherRoomId, type: 'net.example.test', content: 'hi' }); + it("reads events from all rooms", async () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test" }); + const otherRoomEvent = createRoomEvent({ room_id: otherRoomId, type: "net.example.test", content: "hi" }); driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]); driver.readRoomTimeline.mockImplementation(async (rId) => { if (rId === roomId) return [event]; @@ -1456,22 +1456,22 @@ describe('ClientWidgetApi', () => { const request: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', + type: "net.example.test", room_ids: Symbols.AnyRoom, }, }; await loadIframe([ `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - 'org.matrix.msc2762.receive.event:net.example.test', + "org.matrix.msc2762.receive.event:net.example.test", ]); clientWidgetApi.setViewedRoomId(roomId); - emitEvent(new CustomEvent('', { detail: request })); + emitEvent(new CustomEvent("", { detail: request })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(request, { @@ -1481,7 +1481,7 @@ describe('ClientWidgetApi', () => { expect(driver.readRoomTimeline).toHaveBeenCalledWith( roomId, - 'net.example.test', + "net.example.test", undefined, undefined, 0, @@ -1489,7 +1489,7 @@ describe('ClientWidgetApi', () => { ); expect(driver.readRoomTimeline).toHaveBeenCalledWith( otherRoomId, - 'net.example.test', + "net.example.test", undefined, undefined, 0, @@ -1497,40 +1497,40 @@ describe('ClientWidgetApi', () => { ); }); - it('reads state events with any state key', async () => { + it("reads state events with any state key", async () => { driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: 'net.example.test', state_key: 'A' }), - createRoomEvent({ type: 'net.example.test', state_key: 'B' }), + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), ]); const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', + type: "net.example.test", state_key: true, }, }; - await loadIframe(['org.matrix.msc2762.receive.state_event:net.example.test']); - clientWidgetApi.setViewedRoomId('!room-id'); + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test"]); + clientWidgetApi.setViewedRoomId("!room-id"); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { events: [ - createRoomEvent({ type: 'net.example.test', state_key: 'A' }), - createRoomEvent({ type: 'net.example.test', state_key: 'B' }), + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), ], }); }); expect(driver.readRoomTimeline).toBeCalledWith( - '!room-id', - 'net.example.test', + "!room-id", + "net.example.test", undefined, undefined, 0, @@ -1538,21 +1538,21 @@ describe('ClientWidgetApi', () => { ); }); - it('fails to read state events with any state key', async () => { + it("fails to read state events with any state key", async () => { const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', + type: "net.example.test", state_key: true, }, }; await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1563,57 +1563,57 @@ describe('ClientWidgetApi', () => { expect(driver.readRoomTimeline).not.toBeCalled(); }); - it('reads state events with a specific state key', async () => { - driver.readRoomTimeline.mockResolvedValue([createRoomEvent({ type: 'net.example.test', state_key: 'B' })]); + it("reads state events with a specific state key", async () => { + driver.readRoomTimeline.mockResolvedValue([createRoomEvent({ type: "net.example.test", state_key: "B" })]); const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', - state_key: 'B', + type: "net.example.test", + state_key: "B", }, }; - await loadIframe(['org.matrix.msc2762.receive.state_event:net.example.test#B']); - clientWidgetApi.setViewedRoomId('!room-id'); + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#B"]); + clientWidgetApi.setViewedRoomId("!room-id"); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - events: [createRoomEvent({ type: 'net.example.test', state_key: 'B' })], + events: [createRoomEvent({ type: "net.example.test", state_key: "B" })], }); }); expect(driver.readRoomTimeline).toBeCalledWith( - '!room-id', - 'net.example.test', + "!room-id", + "net.example.test", undefined, - 'B', + "B", 0, undefined, ); }); - it('fails to read state events with a specific state key', async () => { + it("fails to read state events with a specific state key", async () => { const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC2876ReadEvents, data: { - type: 'net.example.test', - state_key: 'B', + type: "net.example.test", + state_key: "B", }, }; // Request the capability for the wrong state key - await loadIframe(['org.matrix.msc2762.receive.state_event:net.example.test#A']); + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#A"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1625,39 +1625,39 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc3869.read_relations action', () => { - it('should present as supported api version', () => { + describe("org.matrix.msc3869.read_relations action", () => { + it("should present as supported api version", () => { const event: ISupportedVersionsActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3869]), }); }); - it('should handle and process the request', async () => { + it("should handle and process the request", async () => { driver.readEventRelations.mockResolvedValue({ chunk: [createRoomEvent()], }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: '$event' }, + data: { event_id: "$event" }, }; - await loadIframe(['org.matrix.msc2762.receive.event:m.room.message']); + await loadIframe(["org.matrix.msc2762.receive.event:m.room.message"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1666,7 +1666,7 @@ describe('ClientWidgetApi', () => { }); expect(driver.readEventRelations).toBeCalledWith( - '$event', + "$event", undefined, undefined, undefined, @@ -1677,39 +1677,39 @@ describe('ClientWidgetApi', () => { ); }); - it('should only return events that match requested capabilities', async () => { + it("should only return events that match requested capabilities", async () => { driver.readEventRelations.mockResolvedValue({ chunk: [ createRoomEvent(), - createRoomEvent({ type: 'm.reaction' }), - createRoomEvent({ type: 'net.example.test', state_key: 'A' }), - createRoomEvent({ type: 'net.example.test', state_key: 'B' }), + createRoomEvent({ type: "m.reaction" }), + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), ], }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: '$event' }, + data: { event_id: "$event" }, }; await loadIframe([ - 'org.matrix.msc2762.receive.event:m.room.message', - 'org.matrix.msc2762.receive.state_event:net.example.test#A', + "org.matrix.msc2762.receive.event:m.room.message", + "org.matrix.msc2762.receive.state_event:net.example.test#A", ]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - chunk: [createRoomEvent(), createRoomEvent({ type: 'net.example.test', state_key: 'A' })], + chunk: [createRoomEvent(), createRoomEvent({ type: "net.example.test", state_key: "A" })], }); }); expect(driver.readEventRelations).toBeCalledWith( - '$event', + "$event", undefined, undefined, undefined, @@ -1720,31 +1720,31 @@ describe('ClientWidgetApi', () => { ); }); - it('should accept all options and pass it to the driver', async () => { + it("should accept all options and pass it to the driver", async () => { driver.readEventRelations.mockResolvedValue({ chunk: [], }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { - event_id: '$event', - room_id: '!room-id', - event_type: 'm.room.message', - rel_type: 'm.reference', + event_id: "$event", + room_id: "!room-id", + event_type: "m.room.message", + rel_type: "m.reference", limit: 25, - from: 'from-token', - to: 'to-token', - direction: 'f', + from: "from-token", + to: "to-token", + direction: "f", }, }; - await loadIframe(['org.matrix.msc2762.timeline:!room-id']); + await loadIframe(["org.matrix.msc2762.timeline:!room-id"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1753,127 +1753,127 @@ describe('ClientWidgetApi', () => { }); expect(driver.readEventRelations).toBeCalledWith( - '$event', - '!room-id', - 'm.reference', - 'm.room.message', - 'from-token', - 'to-token', + "$event", + "!room-id", + "m.reference", + "m.room.message", + "from-token", + "to-token", 25, - 'f', + "f", ); }); - it('should reject requests without event_id', async () => { + it("should reject requests without event_id", async () => { const event: IWidgetApiRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - missing event ID' }, + error: { message: "Invalid request - missing event ID" }, }); }); - it('should reject requests with a negative limit', async () => { + it("should reject requests with a negative limit", async () => { const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { - event_id: '$event', + event_id: "$event", limit: -1, }, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - limit out of range' }, + error: { message: "Invalid request - limit out of range" }, }); }); - it('should reject requests when the room timeline was not requested', async () => { + it("should reject requests when the room timeline was not requested", async () => { const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { - event_id: '$event', - room_id: '!another-room-id', + event_id: "$event", + room_id: "!another-room-id", }, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unable to access room timeline: !another-room-id' }, + error: { message: "Unable to access room timeline: !another-room-id" }, }); }); - it('should reject requests when the driver throws an exception', async () => { + it("should reject requests when the driver throws an exception", async () => { driver.readEventRelations.mockRejectedValue( new Error("M_FORBIDDEN: You don't have permission to access that event"), ); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: '$event' }, + data: { event_id: "$event" }, }; await loadIframe(); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unexpected error while reading relations' }, + error: { message: "Unexpected error while reading relations" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.readEventRelations.mockRejectedValue( - new CustomMatrixError('failed to read relations', 403, 'M_FORBIDDEN', { + new CustomMatrixError("failed to read relations", 403, "M_FORBIDDEN", { reason: "You don't have permission to access that event", }), ); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: '$event' }, + data: { event_id: "$event" }, }; await loadIframe(); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Unexpected error while reading relations', + message: "Unexpected error while reading relations", matrix_api_error: { http_status: 403, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_FORBIDDEN', - error: 'failed to read relations', + errcode: "M_FORBIDDEN", + error: "failed to read relations", reason: "You don't have permission to access that event", }, } satisfies IMatrixApiError, @@ -1883,51 +1883,51 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc3973.user_directory_search action', () => { - it('should present as supported api version', () => { + describe("org.matrix.msc3973.user_directory_search action", () => { + it("should present as supported api version", () => { const event: ISupportedVersionsActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3973]), }); }); - it('should handle and process the request', async () => { + it("should handle and process the request", async () => { driver.searchUserDirectory.mockResolvedValue({ limited: true, results: [ { - userId: '@foo:bar.com', + userId: "@foo:bar.com", }, ], }); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: 'foo' }, + data: { search_term: "foo" }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { limited: true, results: [ { - user_id: '@foo:bar.com', + user_id: "@foo:bar.com", display_name: undefined, avatar_url: undefined, }, @@ -1935,61 +1935,61 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.searchUserDirectory).toBeCalledWith('foo', undefined); + expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); }); - it('should accept all options and pass it to the driver', async () => { + it("should accept all options and pass it to the driver", async () => { driver.searchUserDirectory.mockResolvedValue({ limited: false, results: [ { - userId: '@foo:bar.com', + userId: "@foo:bar.com", }, { - userId: '@bar:foo.com', - displayName: 'Bar', - avatarUrl: 'mxc://...', + userId: "@bar:foo.com", + displayName: "Bar", + avatarUrl: "mxc://...", }, ], }); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { - search_term: 'foo', + search_term: "foo", limit: 5, }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { limited: false, results: [ { - user_id: '@foo:bar.com', + user_id: "@foo:bar.com", display_name: undefined, avatar_url: undefined, }, { - user_id: '@bar:foo.com', - display_name: 'Bar', - avatar_url: 'mxc://...', + user_id: "@bar:foo.com", + display_name: "Bar", + avatar_url: "mxc://...", }, ], }); }); - expect(driver.searchUserDirectory).toBeCalledWith('foo', 5); + expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); }); - it('should accept empty search_term', async () => { + it("should accept empty search_term", async () => { driver.searchUserDirectory.mockResolvedValue({ limited: false, results: [], @@ -1997,15 +1997,15 @@ describe('ClientWidgetApi', () => { const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: '' }, + data: { search_term: "" }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2014,126 +2014,126 @@ describe('ClientWidgetApi', () => { }); }); - expect(driver.searchUserDirectory).toBeCalledWith('', undefined); + expect(driver.searchUserDirectory).toBeCalledWith("", undefined); }); - it('should reject requests when the capability was not requested', async () => { + it("should reject requests when the capability was not requested", async () => { const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: 'foo' }, + data: { search_term: "foo" }, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Missing capability' }, + error: { message: "Missing capability" }, }); expect(driver.searchUserDirectory).not.toBeCalled(); }); - it('should reject requests without search_term', async () => { + it("should reject requests without search_term", async () => { const event: IWidgetApiRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: {}, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - missing search term' }, + error: { message: "Invalid request - missing search term" }, }); expect(driver.searchUserDirectory).not.toBeCalled(); }); - it('should reject requests with a negative limit', async () => { + it("should reject requests with a negative limit", async () => { const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { - search_term: 'foo', + search_term: "foo", limit: -1, }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Invalid request - limit out of range' }, + error: { message: "Invalid request - limit out of range" }, }); expect(driver.searchUserDirectory).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { - driver.searchUserDirectory.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); + it("should reject requests when the driver throws an exception", async () => { + driver.searchUserDirectory.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: 'foo' }, + data: { search_term: "foo" }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unexpected error while searching in the user directory' }, + error: { message: "Unexpected error while searching in the user directory" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.searchUserDirectory.mockRejectedValue( - new CustomMatrixError('failed to search the user directory', 429, 'M_LIMIT_EXCEEDED', { - reason: 'Too many requests', + new CustomMatrixError("failed to search the user directory", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", retry_after_ms: 2000, }), ); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: 'foo' }, + data: { search_term: "foo" }, }; - await loadIframe(['org.matrix.msc3973.user_directory_search']); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Unexpected error while searching in the user directory', + message: "Unexpected error while searching in the user directory", matrix_api_error: { http_status: 429, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_LIMIT_EXCEEDED', - error: 'failed to search the user directory', - reason: 'Too many requests', + errcode: "M_LIMIT_EXCEEDED", + error: "failed to search the user directory", + reason: "Too many requests", retry_after_ms: 2000, }, } satisfies IMatrixApiError, @@ -2143,123 +2143,123 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc4039.get_media_config action', () => { - it('should present as supported api version', () => { + describe("org.matrix.msc4039.get_media_config action", () => { + it("should present as supported api version", () => { const event: ISupportedVersionsActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), }); }); - it('should handle and process the request', async () => { + it("should handle and process the request", async () => { driver.getMediaConfig.mockResolvedValue({ - 'm.upload.size': 1000, + "m.upload.size": 1000, }); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - 'm.upload.size': 1000, + "m.upload.size": 1000, }); }); expect(driver.getMediaConfig).toBeCalled(); }); - it('should reject requests when the capability was not requested', async () => { + it("should reject requests when the capability was not requested", async () => { const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Missing capability' }, + error: { message: "Missing capability" }, }); expect(driver.getMediaConfig).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { - driver.getMediaConfig.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); + it("should reject requests when the driver throws an exception", async () => { + driver.getMediaConfig.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unexpected error while getting the media configuration' }, + error: { message: "Unexpected error while getting the media configuration" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.getMediaConfig.mockRejectedValue( - new CustomMatrixError('failed to get the media configuration', 429, 'M_LIMIT_EXCEEDED', { - reason: 'Too many requests', + new CustomMatrixError("failed to get the media configuration", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", retry_after_ms: 2000, }), ); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Unexpected error while getting the media configuration', + message: "Unexpected error while getting the media configuration", matrix_api_error: { http_status: 429, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_LIMIT_EXCEEDED', - error: 'failed to get the media configuration', - reason: 'Too many requests', + errcode: "M_LIMIT_EXCEEDED", + error: "failed to get the media configuration", + reason: "Too many requests", retry_after_ms: 2000, }, } satisfies IMatrixApiError, @@ -2269,17 +2269,17 @@ describe('ClientWidgetApi', () => { }); }); - describe('MSC4039', () => { - it('should present as supported api version', () => { + describe("MSC4039", () => { + it("should present as supported api version", () => { const event: ISupportedVersionsActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), @@ -2287,115 +2287,115 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc4039.upload_file action', () => { - it('should handle and process the request', async () => { + describe("org.matrix.msc4039.upload_file action", () => { + it("should handle and process the request", async () => { driver.uploadFile.mockResolvedValue({ - contentUri: 'mxc://...', + contentUri: "mxc://...", }); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, data: { - file: 'data', + file: "data", }, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - content_uri: 'mxc://...', + content_uri: "mxc://...", }); }); expect(driver.uploadFile).toBeCalled(); }); - it('should reject requests when the capability was not requested', async () => { + it("should reject requests when the capability was not requested", async () => { const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, data: { - file: 'data', + file: "data", }, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Missing capability' }, + error: { message: "Missing capability" }, }); expect(driver.uploadFile).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { - driver.uploadFile.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); + it("should reject requests when the driver throws an exception", async () => { + driver.uploadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, data: { - file: 'data', + file: "data", }, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unexpected error while uploading a file' }, + error: { message: "Unexpected error while uploading a file" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.uploadFile.mockRejectedValue( - new CustomMatrixError('failed to upload a file', 429, 'M_LIMIT_EXCEEDED', { - reason: 'Too many requests', + new CustomMatrixError("failed to upload a file", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", retry_after_ms: 2000, }), ); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, data: { - file: 'data', + file: "data", }, }; - await loadIframe(['org.matrix.msc4039.upload_file']); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Unexpected error while uploading a file', + message: "Unexpected error while uploading a file", matrix_api_error: { http_status: 429, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_LIMIT_EXCEEDED', - error: 'failed to upload a file', - reason: 'Too many requests', + errcode: "M_LIMIT_EXCEEDED", + error: "failed to upload a file", + reason: "Too many requests", retry_after_ms: 2000, }, } satisfies IMatrixApiError, @@ -2405,115 +2405,115 @@ describe('ClientWidgetApi', () => { }); }); - describe('org.matrix.msc4039.download_file action', () => { - it('should handle and process the request', async () => { + describe("org.matrix.msc4039.download_file action", () => { + it("should handle and process the request", async () => { driver.downloadFile.mockResolvedValue({ - file: 'test contents', + file: "test contents", }); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data: { - content_uri: 'mxc://example.com/test_file', + content_uri: "mxc://example.com/test_file", }, }; - await loadIframe(['org.matrix.msc4039.download_file']); + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { - file: 'test contents', + file: "test contents", }); }); - expect(driver.downloadFile).toHaveBeenCalledWith('mxc://example.com/test_file'); + expect(driver.downloadFile).toHaveBeenCalledWith("mxc://example.com/test_file"); }); - it('should reject requests when the capability was not requested', async () => { + it("should reject requests when the capability was not requested", async () => { const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data: { - content_uri: 'mxc://example.com/test_file', + content_uri: "mxc://example.com/test_file", }, }; - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Missing capability' }, + error: { message: "Missing capability" }, }); expect(driver.uploadFile).not.toBeCalled(); }); - it('should reject requests when the driver throws an exception', async () => { - driver.downloadFile.mockRejectedValue(new Error('M_LIMIT_EXCEEDED: Too many requests')); + it("should reject requests when the driver throws an exception", async () => { + driver.downloadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data: { - content_uri: 'mxc://example.com/test_file', + content_uri: "mxc://example.com/test_file", }, }; - await loadIframe(['org.matrix.msc4039.download_file']); + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { message: 'Unexpected error while downloading a file' }, + error: { message: "Unexpected error while downloading a file" }, }); }); }); - it('should reject with Matrix API error response thrown by driver', async () => { + it("should reject with Matrix API error response thrown by driver", async () => { driver.processError.mockImplementation(processCustomMatrixError); driver.downloadFile.mockRejectedValue( - new CustomMatrixError('failed to download a file', 429, 'M_LIMIT_EXCEEDED', { - reason: 'Too many requests', + new CustomMatrixError("failed to download a file", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", retry_after_ms: 2000, }), ); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, - widgetId: 'test', - requestId: '0', + widgetId: "test", + requestId: "0", action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data: { - content_uri: 'mxc://example.com/test_file', + content_uri: "mxc://example.com/test_file", }, }; - await loadIframe(['org.matrix.msc4039.download_file']); + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent('', { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: 'Unexpected error while downloading a file', + message: "Unexpected error while downloading a file", matrix_api_error: { http_status: 429, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_LIMIT_EXCEEDED', - error: 'failed to download a file', - reason: 'Too many requests', + errcode: "M_LIMIT_EXCEEDED", + error: "failed to download a file", + reason: "Too many requests", retry_after_ms: 2000, }, } satisfies IMatrixApiError, @@ -2523,13 +2523,13 @@ describe('ClientWidgetApi', () => { }); }); - it('updates theme', () => { - clientWidgetApi.updateTheme({ name: 'dark' }); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.ThemeChange, { name: 'dark' }); + it("updates theme", () => { + clientWidgetApi.updateTheme({ name: "dark" }); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.ThemeChange, { name: "dark" }); }); - it('updates language', () => { - clientWidgetApi.updateLanguage('tlh'); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.LanguageChange, { lang: 'tlh' }); + it("updates language", () => { + clientWidgetApi.updateLanguage("tlh"); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.LanguageChange, { lang: "tlh" }); }); }); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index f458fb1..b128e1c 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -15,16 +15,16 @@ * limitations under the License. */ -import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; -import { IGetMediaConfigActionFromWidgetResponseData } from '../src/interfaces/GetMediaConfigAction'; -import { IReadRelationsFromWidgetResponseData } from '../src/interfaces/ReadRelationsAction'; -import { ISendEventFromWidgetResponseData } from '../src/interfaces/SendEventAction'; -import { ISupportedVersionsActionResponseData } from '../src/interfaces/SupportedVersionsAction'; -import { IUploadFileActionFromWidgetResponseData } from '../src/interfaces/UploadFileAction'; -import { IDownloadFileActionFromWidgetResponseData } from '../src/interfaces/DownloadFileAction'; -import { IUserDirectorySearchFromWidgetResponseData } from '../src/interfaces/UserDirectorySearchAction'; -import { WidgetApiFromWidgetAction } from '../src/interfaces/WidgetApiAction'; -import { WidgetApi, WidgetApiResponseError } from '../src/WidgetApi'; +import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { IGetMediaConfigActionFromWidgetResponseData } from "../src/interfaces/GetMediaConfigAction"; +import { IReadRelationsFromWidgetResponseData } from "../src/interfaces/ReadRelationsAction"; +import { ISendEventFromWidgetResponseData } from "../src/interfaces/SendEventAction"; +import { ISupportedVersionsActionResponseData } from "../src/interfaces/SupportedVersionsAction"; +import { IUploadFileActionFromWidgetResponseData } from "../src/interfaces/UploadFileAction"; +import { IDownloadFileActionFromWidgetResponseData } from "../src/interfaces/DownloadFileAction"; +import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi"; import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails, @@ -34,7 +34,7 @@ import { IWidgetApiResponseData, UpdateDelayedEventAction, WidgetApiDirection, -} from '../src'; +} from "../src"; type SendRequestArgs = { action: WidgetApiFromWidgetAction; @@ -81,7 +81,7 @@ class ClientTransportHelper { } } -describe('WidgetApi', () => { +describe("WidgetApi", () => { let widgetApi: WidgetApi; let widgetTransportHelper: WidgetTransportHelper; let clientListener: (e: MessageEvent) => void; @@ -93,7 +93,7 @@ describe('WidgetApi', () => { clientListener = (e: MessageEvent): void => { if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response - if ('response' in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request + if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request const request = e.data; clientTrafficHelper.trackRequest(request.action as WidgetApiFromWidgetAction, request.data); @@ -105,22 +105,22 @@ describe('WidgetApi', () => { ...request, response: response, } satisfies IWidgetApiResponse, - '*', + "*", ); } }; - window.addEventListener('message', clientListener); + window.addEventListener("message", clientListener); - widgetApi = new WidgetApi('WidgetApi-test', '*'); + widgetApi = new WidgetApi("WidgetApi-test", "*"); widgetApi.start(); }); afterEach(() => { - window.removeEventListener('message', clientListener); + window.removeEventListener("message", clientListener); }); - describe('readEventRelations', () => { - it('should forward the request to the ClientWidgetApi', async () => { + describe("readEventRelations", () => { + it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], } as ISupportedVersionsActionResponseData); @@ -130,14 +130,14 @@ describe('WidgetApi', () => { await expect( widgetApi.readEventRelations( - '$event', - '!room-id', - 'm.reference', - 'm.room.message', + "$event", + "!room-id", + "m.reference", + "m.room.message", 25, - 'from-token', - 'to-token', - 'f', + "from-token", + "to-token", + "f", ), ).resolves.toEqual({ chunk: [], @@ -147,33 +147,33 @@ describe('WidgetApi', () => { expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { - event_id: '$event', - room_id: '!room-id', - rel_type: 'm.reference', - event_type: 'm.room.message', + event_id: "$event", + room_id: "!room-id", + rel_type: "m.reference", + event_type: "m.room.message", limit: 25, - from: 'from-token', - to: 'to-token', - direction: 'f', + from: "from-token", + to: "to-token", + direction: "f", }, } satisfies SendRequestArgs); }); - it('should reject the request if the api is not supported', async () => { + it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); await expect( widgetApi.readEventRelations( - '$event', - '!room-id', - 'm.reference', - 'm.room.message', + "$event", + "!room-id", + "m.reference", + "m.room.message", 25, - 'from-token', - 'to-token', - 'f', + "from-token", + "to-token", + "f", ), - ).rejects.toThrow('The read_relations action is not supported by the client.'); + ).rejects.toThrow("The read_relations action is not supported by the client."); const request = widgetTransportHelper.nextTrackedRequest(); expect(request).not.toBeUndefined(); @@ -183,29 +183,29 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ - error: { message: 'An error occurred' }, + error: { message: "An error occurred" }, } as IWidgetApiErrorResponseData); await expect( widgetApi.readEventRelations( - '$event', - '!room-id', - 'm.reference', - 'm.room.message', + "$event", + "!room-id", + "m.reference", + "m.room.message", 25, - 'from-token', - 'to-token', - 'f', + "from-token", + "to-token", + "f", ), - ).rejects.toThrow('An error occurred'); + ).rejects.toThrow("An error occurred"); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], } as ISupportedVersionsActionResponseData); @@ -214,249 +214,249 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); await expect( widgetApi.readEventRelations( - '$event', - '!room-id', - 'm.reference', - 'm.room.message', + "$event", + "!room-id", + "m.reference", + "m.room.message", 25, - 'from-token', - 'to-token', - 'f', + "from-token", + "to-token", + "f", ), - ).rejects.toThrow(new WidgetApiResponseError('An error occurred', errorDetails)); + ).rejects.toThrow(new WidgetApiResponseError("An error occurred", errorDetails)); }); }); - describe('sendEvent', () => { - it('sends message events', async () => { + describe("sendEvent", () => { + it("sends message events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - event_id: '$event', + room_id: "!room-id", + event_id: "$event", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).resolves.toEqual({ - room_id: '!room-id', - event_id: '$event', + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", }); }); - it('sends state events', async () => { + it("sends state events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - event_id: '$event', + room_id: "!room-id", + event_id: "$event", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id')).resolves.toEqual({ - room_id: '!room-id', - event_id: '$event', + await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id")).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", }); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ - error: { message: 'An error occurred' }, + error: { message: "An error occurred" }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).rejects.toThrow( - 'An error occurred', + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( + "An error occurred", ); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id')).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('delayed sendEvent', () => { - it('sends delayed message events', async () => { + describe("delayed sendEvent", () => { + it("sends delayed message events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - delay_id: 'id', + room_id: "!room-id", + delay_id: "id", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 2000)).resolves.toEqual({ - room_id: '!room-id', - delay_id: 'id', + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000)).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", }); }); - it('sends delayed state events', async () => { + it("sends delayed state events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - delay_id: 'id', + room_id: "!room-id", + delay_id: "id", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id', 2000)).resolves.toEqual({ - room_id: '!room-id', - delay_id: 'id', + await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 2000)).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", }); }); - it('sends delayed child action message events', async () => { + it("sends delayed child action message events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - delay_id: 'id', + room_id: "!room-id", + delay_id: "id", } as ISendEventFromWidgetResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).resolves.toEqual({ - room_id: '!room-id', - delay_id: 'id', + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", }); }); - it('sends delayed child action state events', async () => { + it("sends delayed child action state events", async () => { widgetTransportHelper.queueResponse({ - room_id: '!room-id', - delay_id: 'id', + room_id: "!room-id", + delay_id: "id", } as ISendEventFromWidgetResponseData); await expect( - widgetApi.sendStateEvent('m.room.topic', '', {}, '!room-id', 1000, undefined), + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 1000, undefined), ).resolves.toEqual({ - room_id: '!room-id', - delay_id: 'id', + room_id: "!room-id", + delay_id: "id", }); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ - error: { message: 'An error occurred' }, + error: { message: "An error occurred" }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).rejects.toThrow( - 'An error occurred', + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( + "An error occurred", ); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.sendRoomEvent('m.room.message', {}, '!room-id', 1000, undefined)).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('updateDelayedEvent', () => { - it('updates delayed events', async () => { + describe("updateDelayedEvent", () => { + it("updates delayed events", async () => { widgetTransportHelper.queueResponse({}); - await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).resolves.toEqual({}); + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).resolves.toEqual({}); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ - error: { message: 'An error occurred' }, + error: { message: "An error occurred" }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).rejects.toThrow( - 'An error occurred', + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( + "An error occurred", ); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.updateDelayedEvent('id', UpdateDelayedEventAction.Send)).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('getClientVersions', () => { + describe("getClientVersions", () => { beforeEach(() => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869, UnstableApiVersion.MSC2762], } as ISupportedVersionsActionResponseData); }); - it('should request supported client versions', async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); + it("should request supported client versions", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); }); - it('should cache supported client versions on successive calls', async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); + it("should cache supported client versions on successive calls", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); - await expect(widgetApi.getClientVersions()).resolves.toEqual(['org.matrix.msc3869', 'org.matrix.msc2762']); + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); }); }); - describe('searchUserDirectory', () => { - it('should forward the request to the ClientWidgetApi', async () => { + describe("searchUserDirectory", () => { + it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], } as ISupportedVersionsActionResponseData); @@ -465,7 +465,7 @@ describe('WidgetApi', () => { results: [], } as IUserDirectorySearchFromWidgetResponseData); - await expect(widgetApi.searchUserDirectory('foo', 10)).resolves.toEqual({ + await expect(widgetApi.searchUserDirectory("foo", 10)).resolves.toEqual({ limited: false, results: [], }); @@ -474,17 +474,17 @@ describe('WidgetApi', () => { expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { - search_term: 'foo', + search_term: "foo", limit: 10, }, } satisfies SendRequestArgs); }); - it('should reject the request if the api is not supported', async () => { + it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow( - 'The user_directory_search action is not supported by the client.', + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( + "The user_directory_search action is not supported by the client.", ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -495,16 +495,16 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); + widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow('An error occurred'); + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow("An error occurred"); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], } as ISupportedVersionsActionResponseData); @@ -513,38 +513,38 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.searchUserDirectory('foo', 10)).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('getMediaConfig', () => { - it('should forward the request to the ClientWidgetApi', async () => { + describe("getMediaConfig", () => { + it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ - 'm.upload.size': 1000, + "m.upload.size": 1000, } as IGetMediaConfigActionFromWidgetResponseData); await expect(widgetApi.getMediaConfig()).resolves.toEqual({ - 'm.upload.size': 1000, + "m.upload.size": 1000, }); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); @@ -554,11 +554,11 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should reject the request if the api is not supported', async () => { + it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( - 'The get_media_config action is not supported by the client.', + "The get_media_config action is not supported by the client.", ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -569,16 +569,16 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); + widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - await expect(widgetApi.getMediaConfig()).rejects.toThrow('An error occurred'); + await expect(widgetApi.getMediaConfig()).rejects.toThrow("An error occurred"); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); @@ -587,52 +587,52 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('uploadFile', () => { - it('should forward the request to the ClientWidgetApi', async () => { + describe("uploadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ - content_uri: 'mxc://...', + content_uri: "mxc://...", } as IUploadFileActionFromWidgetResponseData); - await expect(widgetApi.uploadFile('data')).resolves.toEqual({ - content_uri: 'mxc://...', + await expect(widgetApi.uploadFile("data")).resolves.toEqual({ + content_uri: "mxc://...", }); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { file: 'data' }, + data: { file: "data" }, } satisfies SendRequestArgs); }); - it('should reject the request if the api is not supported', async () => { + it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.uploadFile('data')).rejects.toThrow( - 'The upload_file action is not supported by the client.', + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "The upload_file action is not supported by the client.", ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -643,16 +643,16 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); + widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - await expect(widgetApi.uploadFile('data')).rejects.toThrow('An error occurred'); + await expect(widgetApi.uploadFile("data")).rejects.toThrow("An error occurred"); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); @@ -661,50 +661,50 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.uploadFile('data')).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); - describe('downloadFile', () => { - it('should forward the request to the ClientWidgetApi', async () => { + describe("downloadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ file: 'test contents' } as IDownloadFileActionFromWidgetResponseData); + widgetTransportHelper.queueResponse({ file: "test contents" } as IDownloadFileActionFromWidgetResponseData); - await expect(widgetApi.downloadFile('mxc://example.com/test_file')).resolves.toEqual({ - file: 'test contents', + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).resolves.toEqual({ + file: "test contents", }); expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { content_uri: 'mxc://example.com/test_file' }, + data: { content_uri: "mxc://example.com/test_file" }, } satisfies SendRequestArgs); }); - it('should reject the request if the api is not supported', async () => { + it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow( - 'The download_file action is not supported by the client.', + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( + "The download_file action is not supported by the client.", ); const request = widgetTransportHelper.nextTrackedRequest(); @@ -715,16 +715,16 @@ describe('WidgetApi', () => { } satisfies SendRequestArgs); }); - it('should handle an error', async () => { + it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: 'An error occurred' } }); + widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow('An error occurred'); + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow("An error occurred"); }); - it('should handle an error with details', async () => { + it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], } as ISupportedVersionsActionResponseData); @@ -733,23 +733,23 @@ describe('WidgetApi', () => { matrix_api_error: { http_status: 400, http_headers: {}, - url: '', + url: "", response: { - errcode: 'M_UNKNOWN', - error: 'Unknown error', + errcode: "M_UNKNOWN", + error: "Unknown error", }, }, }; widgetTransportHelper.queueResponse({ error: { - message: 'An error occurred', + message: "An error occurred", ...errorDetails, }, } as IWidgetApiErrorResponseData); - await expect(widgetApi.downloadFile('mxc://example.com/test_file')).rejects.toThrow( - new WidgetApiResponseError('An error occurred', errorDetails), + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), ); }); }); diff --git a/test/url-template-test.ts b/test/url-template-test.ts index cb4bafa..3f28df8 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -14,44 +14,44 @@ * limitations under the License. */ -import { runTemplate } from '../src'; +import { runTemplate } from "../src"; -describe('runTemplate', () => { - it('should replace device id template in url', () => { - const url = 'https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id'; +describe("runTemplate", () => { + it("should replace device id template in url", () => { + const url = "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; const replacedUrl = runTemplate( url, { - id: 'widget-id', - creatorUserId: '@user-id', - type: 'type', + id: "widget-id", + creatorUserId: "@user-id", + type: "type", url, }, { - deviceId: 'my-device-id', - currentUserId: '@user-id', + deviceId: "my-device-id", + currentUserId: "@user-id", }, ); - expect(replacedUrl).toBe('https://localhost/?my-query#device_id=my-device-id'); + expect(replacedUrl).toBe("https://localhost/?my-query#device_id=my-device-id"); }); - it('should replace base url template in url', () => { - const url = 'https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url'; + it("should replace base url template in url", () => { + const url = "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; const replacedUrl = runTemplate( url, { - id: 'widget-id', - creatorUserId: '@user-id', - type: 'type', + id: "widget-id", + creatorUserId: "@user-id", + type: "type", url, }, { - currentUserId: '@user-id', - baseUrl: 'https://localhost/api', + currentUserId: "@user-id", + baseUrl: "https://localhost/api", }, ); - expect(replacedUrl).toBe('https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi'); + expect(replacedUrl).toBe("https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi"); }); }); From 5e9810f2930267d93681b4e91682a26ce8a38a28 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:23:51 +0100 Subject: [PATCH 05/11] more 4 tab files --- .eslintrc.js | 96 ++++++++++++++++----------------- .github/workflows/sonarqube.yml | 24 ++++----- examples/widget/utils.js | 14 ++--- package.json | 4 +- 4 files changed, 70 insertions(+), 68 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 02a10fe..96ca83a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,52 +1,52 @@ module.exports = { - plugins: ["matrix-org"], - extends: ["plugin:matrix-org/babel"], - parserOptions: { - project: ["./tsconfig-dev.json"], - }, - env: { - browser: true, - }, - rules: { - "no-var": ["warn"], - "prefer-rest-params": ["warn"], - "prefer-spread": ["warn"], - "one-var": ["warn"], - "padded-blocks": ["warn"], - "no-extend-native": ["warn"], - "camelcase": ["warn"], - "no-multi-spaces": ["error", { ignoreEOLComments: true }], - "space-before-function-paren": [ - "error", - { - anonymous: "never", - named: "never", - asyncArrow: "always", - }, - ], - "arrow-parens": "off", - "prefer-promise-reject-errors": "off", - "quotes": "off", - "indent": "off", - "no-constant-condition": "off", - "no-async-promise-executor": "off", - }, - overrides: [ - { - files: ["src/**/*.ts", "test/**/*.ts"], - extends: ["plugin:matrix-org/typescript"], - rules: { - // TypeScript has its own version of this - "babel/no-invalid-this": "off", - - "quotes": "off", - }, + plugins: ["matrix-org"], + extends: ["plugin:matrix-org/babel"], + parserOptions: { + project: ["./tsconfig-dev.json"], }, - { - files: ["src/interfaces/**/*.ts"], - rules: { - "@typescript-eslint/no-empty-object-type": "off", - }, + env: { + browser: true, }, - ], + rules: { + "no-var": ["warn"], + "prefer-rest-params": ["warn"], + "prefer-spread": ["warn"], + "one-var": ["warn"], + "padded-blocks": ["warn"], + "no-extend-native": ["warn"], + "camelcase": ["warn"], + "no-multi-spaces": ["error", { ignoreEOLComments: true }], + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always", + }, + ], + "arrow-parens": "off", + "prefer-promise-reject-errors": "off", + "quotes": "off", + "indent": "off", + "no-constant-condition": "off", + "no-async-promise-executor": "off", + }, + overrides: [ + { + files: ["src/**/*.ts", "test/**/*.ts"], + extends: ["plugin:matrix-org/typescript"], + rules: { + // TypeScript has its own version of this + "babel/no-invalid-this": "off", + + "quotes": "off", + }, + }, + { + files: ["src/interfaces/**/*.ts"], + rules: { + "@typescript-eslint/no-empty-object-type": "off", + }, + }, + ], }; diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 2773eaa..c539966 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,16 +1,16 @@ name: SonarQube on: - workflow_run: - workflows: ["Build and test"] - types: - - completed + workflow_run: + workflows: ["Build and test"] + types: + - completed concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true jobs: - sonarqube: - name: 🩻 SonarQube - uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + sonarqube: + name: 🩻 SonarQube + uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/examples/widget/utils.js b/examples/widget/utils.js index 0a3cd49..705a6f0 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,17 +15,17 @@ */ function parseFragment() { - const fragmentString = window.location.hash || "?"; - return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); + const fragmentString = window.location.hash || "?"; + return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); } function assertParam(fragment, name) { - const val = fragment.get(name); - if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); - return val; + const val = fragment.get(name); + if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); + return val; } function handleError(e) { - console.error(e); - document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; + console.error(e); + document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; } diff --git a/package.json b/package.json index 3e4a189..d7d78c2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ { "files": [ "src/**/*.ts", - "test/**/*.ts" + "*.js", + "test/**/*.ts", + "*.yml" ], "options": { "tabWidth": 4 From 8368650ee1e4548be653ca2344ad2d47653e34be Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:48:52 +0100 Subject: [PATCH 06/11] use shared config --- .eslintrc.js | 100 +- .github/workflows/sonarqube.yml | 24 +- README.md | 30 +- examples/widget/index.html | 7 +- examples/widget/utils.js | 18 +- package.json | 19 - src/ClientWidgetApi.ts | 2345 +++++++++++-------- src/driver/WidgetDriver.ts | 718 +++--- src/interfaces/ApiVersion.ts | 56 +- src/interfaces/Capabilities.ts | 91 +- src/interfaces/CapabilitiesAction.ts | 63 +- src/interfaces/ContentLoadedAction.ts | 14 +- src/interfaces/DownloadFileAction.ts | 22 +- src/interfaces/GetMediaConfigAction.ts | 20 +- src/interfaces/GetOpenIDAction.ts | 28 +- src/interfaces/ICustomWidgetData.ts | 8 +- src/interfaces/IJitsiWidgetData.ts | 26 +- src/interfaces/IRoomAccountData.ts | 6 +- src/interfaces/IRoomEvent.ts | 16 +- src/interfaces/IStickerpickerWidgetData.ts | 2 +- src/interfaces/IWidget.ts | 76 +- src/interfaces/IWidgetApiErrorResponse.ts | 54 +- src/interfaces/IWidgetApiRequest.ts | 22 +- src/interfaces/IWidgetApiResponse.ts | 9 +- src/interfaces/LanguageChangeAction.ts | 20 +- src/interfaces/ModalButtonKind.ts | 10 +- src/interfaces/ModalWidgetActions.ts | 65 +- src/interfaces/NavigateAction.ts | 8 +- src/interfaces/OpenIDCredentialsAction.ts | 22 +- src/interfaces/ReadEventAction.ts | 26 +- src/interfaces/ReadRelationsAction.ts | 40 +- src/interfaces/ReadRoomAccountDataAction.ts | 24 +- src/interfaces/ScreenshotAction.ts | 13 +- src/interfaces/SendEventAction.ts | 56 +- src/interfaces/SendToDeviceAction.ts | 51 +- 35 files changed, 2272 insertions(+), 1837 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 96ca83a..d3eb235 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,52 +1,56 @@ module.exports = { - plugins: ["matrix-org"], - extends: ["plugin:matrix-org/babel"], - parserOptions: { - project: ["./tsconfig-dev.json"], - }, - env: { - browser: true, + plugins: ["matrix-org"], + extends: [ + "plugin:matrix-org/babel", + "plugin:matrix-org/typescript", + "prettier", + ], + parserOptions: { + project: ["./tsconfig-dev.json"], + }, + env: { + browser: true, + }, + rules: { + "no-var": ["warn"], + "prefer-rest-params": ["warn"], + "prefer-spread": ["warn"], + "one-var": ["warn"], + "padded-blocks": ["warn"], + "no-extend-native": ["warn"], + camelcase: ["warn"], + "no-multi-spaces": ["error", { ignoreEOLComments: true }], + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always", + }, + ], + "arrow-parens": "off", + "prefer-promise-reject-errors": "off", + quotes: "off", + indent: "off", + "no-constant-condition": "off", + "no-async-promise-executor": "off", + }, + overrides: [ + { + files: ["src/**/*.ts", "test/**/*.ts"], + extends: ["plugin:matrix-org/typescript"], + rules: { + // TypeScript has its own version of this + "babel/no-invalid-this": "off", + + quotes: "off", + }, }, - rules: { - "no-var": ["warn"], - "prefer-rest-params": ["warn"], - "prefer-spread": ["warn"], - "one-var": ["warn"], - "padded-blocks": ["warn"], - "no-extend-native": ["warn"], - "camelcase": ["warn"], - "no-multi-spaces": ["error", { ignoreEOLComments: true }], - "space-before-function-paren": [ - "error", - { - anonymous: "never", - named: "never", - asyncArrow: "always", - }, - ], - "arrow-parens": "off", - "prefer-promise-reject-errors": "off", - "quotes": "off", - "indent": "off", - "no-constant-condition": "off", - "no-async-promise-executor": "off", + { + files: ["src/interfaces/**/*.ts"], + rules: { + "@typescript-eslint/no-empty-object-type": "off", + }, }, - overrides: [ - { - files: ["src/**/*.ts", "test/**/*.ts"], - extends: ["plugin:matrix-org/typescript"], - rules: { - // TypeScript has its own version of this - "babel/no-invalid-this": "off", - - "quotes": "off", - }, - }, - { - files: ["src/interfaces/**/*.ts"], - rules: { - "@typescript-eslint/no-empty-object-type": "off", - }, - }, - ], + ], }; diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index c539966..2773eaa 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,16 +1,16 @@ name: SonarQube on: - workflow_run: - workflows: ["Build and test"] - types: - - completed + workflow_run: + workflows: ["Build and test"] + types: + - completed concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true jobs: - sonarqube: - name: 🩻 SonarQube - uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + sonarqube: + name: 🩻 SonarQube + uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/README.md b/README.md index 6016876..6f9b715 100644 --- a/README.md +++ b/README.md @@ -44,16 +44,22 @@ api.requestCapability(MatrixCapabilities.Screenshots); api.requestCapabilities(StickerpickerCapabilities); // Add custom action handlers (if needed) -api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, {}); -}); -api.on("action:com.example.my_action", (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, { custom: "reply" }); -}); +api.on( + `action:${WidgetApiToWidgetAction.UpdateVisibility}`, + (ev: CustomEvent) => { + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, {}); + }, +); +api.on( + "action:com.example.my_action", + (ev: CustomEvent) => { + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, { custom: "reply" }); + }, +); // Start the messaging api.start(); @@ -82,7 +88,9 @@ const api = new ClientWidgetApi(widget, iframe, driver); // The API is automatically started, so we just have to wait for a ready before doing something api.on("ready", () => { - api.updateVisibility(true).then(() => console.log("Widget knows it is visible now")); + api + .updateVisibility(true) + .then(() => console.log("Widget knows it is visible now")); api.transport.send("com.example.my_action", { isExample: true }); }); diff --git a/examples/widget/index.html b/examples/widget/index.html index 9b9ceef..d1f62cc 100644 --- a/examples/widget/index.html +++ b/examples/widget/index.html @@ -65,7 +65,9 @@ // Set up the widget API as soon as possible to avoid problems with the client const widgetApi = new mxwidgets.WidgetApi(widgetId, targetOrigin); - widgetApi.requestCapability(mxwidgets.MatrixCapabilities.AlwaysOnScreen); + widgetApi.requestCapability( + mxwidgets.MatrixCapabilities.AlwaysOnScreen, + ); widgetApi.on("ready", function () { // Fill in the basic widget details now that we're allowed to operate. @@ -91,7 +93,8 @@ } function updateStickyState() { - document.getElementById("stickyState").innerText = isSticky.toString(); + document.getElementById("stickyState").innerText = + isSticky.toString(); } function sendStickyState() { diff --git a/examples/widget/utils.js b/examples/widget/utils.js index 705a6f0..d83bd4a 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,17 +15,21 @@ */ function parseFragment() { - const fragmentString = window.location.hash || "?"; - return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); + const fragmentString = window.location.hash || "?"; + return new URLSearchParams( + fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0)), + ); } function assertParam(fragment, name) { - const val = fragment.get(name); - if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); - return val; + const val = fragment.get(name); + if (!val) + throw new Error(`${name} is not present in URL - cannot load widget`); + return val; } function handleError(e) { - console.error(e); - document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; + console.error(e); + document.getElementById("container").innerText = + "There was an error with the widget. See JS console for details."; } diff --git a/package.json b/package.json index d7d78c2..cda642c 100644 --- a/package.json +++ b/package.json @@ -23,25 +23,6 @@ "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", "test": "jest" }, - "prettier": { - "printWidth": 120, - "tabWidth": 2, - "quoteProps": "consistent", - "trailingComma": "all", - "overrides": [ - { - "files": [ - "src/**/*.ts", - "*.js", - "test/**/*.ts", - "*.yml" - ], - "options": { - "tabWidth": 4 - } - } - ] - }, "files": [ "src", "lib", diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index d3bf49d..fcbb8c3 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -20,93 +20,114 @@ 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 { + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./interfaces/IWidgetApiRequest"; import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./interfaces/WidgetApiAction"; import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse"; import { - Capability, - MatrixCapabilities, - getTimelineRoomIDFromCapability, - isTimelineCapability, + Capability, + MatrixCapabilities, + getTimelineRoomIDFromCapability, + isTimelineCapability, } from "./interfaces/Capabilities"; -import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, WidgetDriver } from "./driver/WidgetDriver"; import { - ICapabilitiesActionResponseData, - INotifyCapabilitiesActionRequestData, - IRenegotiateCapabilitiesActionRequest, + IOpenIDUpdate, + ISendEventDetails, + ISendDelayedEventDetails, + WidgetDriver, +} from "./driver/WidgetDriver"; +import { + ICapabilitiesActionResponseData, + INotifyCapabilitiesActionRequestData, + IRenegotiateCapabilitiesActionRequest, } from "./interfaces/CapabilitiesAction"; import { - ISupportedVersionsActionRequest, - ISupportedVersionsActionResponseData, + 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, + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponseData, +} from "./interfaces/IWidgetApiResponse"; +import { + IModalWidgetButtonClickedRequestData, + IModalWidgetOpenRequestData, + IModalWidgetOpenRequestDataButton, + IModalWidgetReturnData, } from "./interfaces/ModalWidgetActions"; import { - ISendEventFromWidgetActionRequest, - ISendEventFromWidgetResponseData, - ISendEventToWidgetRequestData, + ISendEventFromWidgetActionRequest, + ISendEventFromWidgetResponseData, + ISendEventToWidgetRequestData, } from "./interfaces/SendEventAction"; import { - ISendToDeviceFromWidgetActionRequest, - ISendToDeviceFromWidgetResponseData, - ISendToDeviceToWidgetRequestData, + ISendToDeviceFromWidgetActionRequest, + ISendToDeviceFromWidgetResponseData, + ISendToDeviceToWidgetRequestData, } from "./interfaces/SendToDeviceAction"; -import { EventDirection, EventKind, WidgetEventCapability } from "./models/WidgetEventCapability"; +import { + EventDirection, + EventKind, + WidgetEventCapability, +} from "./models/WidgetEventCapability"; import { IRoomEvent } from "./interfaces/IRoomEvent"; import { IRoomAccountData } from "./interfaces/IRoomAccountData"; import { - IGetOpenIDActionRequest, - IGetOpenIDActionResponseData, - IOpenIDCredentials, - OpenIDRequestState, + 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, + IReadEventFromWidgetActionRequest, + IReadEventFromWidgetResponseData, +} from "./interfaces/ReadEventAction"; +import { + ITurnServer, + IWatchTurnServersRequest, + IUnwatchTurnServersRequest, + IUpdateTurnServersRequestData, } from "./interfaces/TurnServerActions"; import { Symbols } from "./Symbols"; import { - IReadRelationsFromWidgetActionRequest, - IReadRelationsFromWidgetResponseData, + IReadRelationsFromWidgetActionRequest, + IReadRelationsFromWidgetResponseData, } from "./interfaces/ReadRelationsAction"; import { - IUserDirectorySearchFromWidgetActionRequest, - IUserDirectorySearchFromWidgetResponseData, + IUserDirectorySearchFromWidgetActionRequest, + IUserDirectorySearchFromWidgetResponseData, } from "./interfaces/UserDirectorySearchAction"; import { - IReadRoomAccountDataFromWidgetActionRequest, - IReadRoomAccountDataFromWidgetResponseData, + IReadRoomAccountDataFromWidgetActionRequest, + IReadRoomAccountDataFromWidgetResponseData, } from "./interfaces/ReadRoomAccountDataAction"; import { - IGetMediaConfigActionFromWidgetActionRequest, - IGetMediaConfigActionFromWidgetResponseData, + IGetMediaConfigActionFromWidgetActionRequest, + IGetMediaConfigActionFromWidgetResponseData, } from "./interfaces/GetMediaConfigAction"; import { - IUpdateDelayedEventFromWidgetActionRequest, - UpdateDelayedEventAction, + IUpdateDelayedEventFromWidgetActionRequest, + UpdateDelayedEventAction, } from "./interfaces/UpdateDelayedEventAction"; import { - IUploadFileActionFromWidgetActionRequest, - IUploadFileActionFromWidgetResponseData, + IUploadFileActionFromWidgetActionRequest, + IUploadFileActionFromWidgetResponseData, } from "./interfaces/UploadFileAction"; import { - IDownloadFileActionFromWidgetActionRequest, - IDownloadFileActionFromWidgetResponseData, + IDownloadFileActionFromWidgetActionRequest, + IDownloadFileActionFromWidgetResponseData, } from "./interfaces/DownloadFileAction"; import { IThemeChangeActionRequestData } from "./interfaces/ThemeChangeAction"; import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction"; @@ -136,1023 +157,1310 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" * 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; - private contentLoadedWaitTimer?: ReturnType; - // Stores pending requests to push a room's state to the widget - private pushRoomStateTasks = new Set>(); - // Room ID → event type → state key → events to be pushed - private pushRoomStateResult = new Map>>(); - private flushRoomStateTask: Promise | 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 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; + private contentLoadedWaitTimer?: ReturnType; + // Stores pending requests to push a room's state to the widget + private pushRoomStateTasks = new Set>(); + // Room ID → event type → state key → events to be pushed + private pushRoomStateResult = new Map< + string, + Map> + >(); + private flushRoomStateTask: Promise | 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"); } - - public hasCapability(capability: Capability): boolean { - return this.allowedCapabilities.has(capability); + if (!widget) { + throw new Error("Invalid widget"); } - - public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean { - return ( - this.hasCapability(`org.matrix.msc2762.timeline:${Symbols.AnyRoom}`) || - this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`) + 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 canReceiveRoomAccountData(eventType: string): boolean { + return this.allowedEvents.some((e) => + e.matchesAsRoomAccountData(EventDirection.Receive, eventType), + ); + } + + public stop(): void { + this.isStopped = true; + this.transport.stop(); + } + + private beginCapabilities(): void { + // 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) => { + this.allowCapabilities([...allowedCaps], requestedCaps); + this.emit("ready"); + }) + .catch((e) => { + this.emit("error:preparing", e); + }); + } + + private allowCapabilities(allowed: string[], requested: string[]): void { + console.log(`Widget ${this.widget.id} is allowed capabilities:`, allowed); + + for (const c of allowed) this.allowedCapabilities.add(c); + const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); + this.allowedEvents.push(...allowedEvents); + + this.transport + .send(WidgetApiToWidgetAction.NotifyCapabilities, < + INotifyCapabilitiesActionRequestData + >{ + requested, + approved: Array.from(this.allowedCapabilities), + }) + .catch((e) => { + console.warn( + "non-fatal error notifying widget of approved capabilities:", + e, ); + }) + .then(() => { + this.emit("capabilitiesNotified"); + }); + + // Push the initial room state for all rooms with a timeline capability + for (const c of allowed) { + if (isTimelineCapability(c)) { + const roomId = getTimelineRoomIDFromCapability(c); + if (roomId === Symbols.AnyRoom) { + for (const roomId of this.driver.getKnownRooms()) + this.pushRoomState(roomId); + } else { + this.pushRoomState(roomId); + } + } } - - public canSendRoomEvent(eventType: string, msgtype: string | null = null): boolean { - return this.allowedEvents.some((e) => e.matchesAsRoomEvent(EventDirection.Send, eventType, msgtype)); + // If new events are allowed and the currently viewed room isn't covered + // by a timeline capability, then we know that there could be some state + // in the viewed room that the widget hasn't learned about yet- push it. + if ( + allowedEvents.length > 0 && + this.viewedRoomId !== null && + !this.canUseRoomTimeline(this.viewedRoomId) + ) { + this.pushRoomState(this.viewedRoomId); } - - public canSendStateEvent(eventType: string, stateKey: string): boolean { - return this.allowedEvents.some((e) => e.matchesAsStateEvent(EventDirection.Send, eventType, stateKey)); + } + + private onIframeLoad(ev: Event): void { + 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. + console.log( + "waitForIframeLoad is false: waiting for widget to send contentLoaded", + ); + this.contentLoadedWaitTimer = setTimeout(() => { + console.error( + "Widget specified waitForIframeLoad=false but timed out waiting for contentLoaded event!", + ); + }, 10000); + this.contentLoadedActionSent = false; } + } - public canSendToDeviceEvent(eventType: string): boolean { - return this.allowedEvents.some((e) => e.matchesAsToDeviceEvent(EventDirection.Send, eventType)); + private handleContentLoadedAction(action: IContentLoadedActionRequest): void { + if (this.contentLoadedWaitTimer !== undefined) { + clearTimeout(this.contentLoadedWaitTimer); + this.contentLoadedWaitTimer = undefined; } - - public canReceiveRoomEvent(eventType: string, msgtype: string | null = null): boolean { - return this.allowedEvents.some((e) => e.matchesAsRoomEvent(EventDirection.Receive, eventType, msgtype)); + if (this.contentLoadedActionSent) { + throw new Error( + "Improper sequence: ContentLoaded Action can only be sent once after the widget loaded " + + "and should only be used if waitForIframeLoad is false (default=true)", + ); } - - public canReceiveStateEvent(eventType: string, stateKey: string | null): boolean { - return this.allowedEvents.some((e) => e.matchesAsStateEvent(EventDirection.Receive, eventType, stateKey)); + if (this.widget.waitForIframeLoad) { + this.transport.reply(action, { + error: { + message: + "Improper sequence: not expecting ContentLoaded event if " + + "waitForIframeLoad is true (default=true)", + }, + }); + } else { + this.transport.reply(action, {}); + this.beginCapabilities(); } - - public canReceiveToDeviceEvent(eventType: string): boolean { - return this.allowedEvents.some((e) => e.matchesAsToDeviceEvent(EventDirection.Receive, eventType)); + this.contentLoadedActionSent = true; + } + + private replyVersions(request: ISupportedVersionsActionRequest): void { + this.transport.reply(request, { + supported_versions: CurrentApiVersions, + }); + } + + private handleCapabilitiesRenegotiate( + request: IRenegotiateCapabilitiesActionRequest, + ): void { + // 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 - skip validation + this.allowCapabilities([], []); } - public canReceiveRoomAccountData(eventType: string): boolean { - return this.allowedEvents.some((e) => e.matchesAsRoomAccountData(EventDirection.Receive, eventType)); + this.driver + .validateCapabilities(newlyRequested) + .then((allowed) => + this.allowCapabilities([...allowed], [...newlyRequested]), + ); + } + + private handleNavigate(request: INavigateActionRequest): void { + if (!this.hasCapability(MatrixCapabilities.MSC2931Navigate)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - public stop(): void { - this.isStopped = true; - this.transport.stop(); + if ( + !request.data?.uri || + !request.data?.uri.toString().startsWith("https://matrix.to/#") + ) { + return this.transport.reply(request, { + error: { message: "Invalid matrix.to URI" }, + }); } - private beginCapabilities(): void { - // 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) => { - this.allowCapabilities([...allowedCaps], requestedCaps); - this.emit("ready"); - }) - .catch((e) => { - this.emit("error:preparing", e); - }); + const onErr = (e: unknown): void => { + console.error("[ClientWidgetApi] Failed to handle navigation: ", e); + this.handleDriverError(e, request, "Error handling navigation"); + }; + + try { + this.driver + .navigate(request.data.uri.toString()) + .catch((e: unknown) => onErr(e)) + .then(() => { + return this.transport.reply( + request, + {}, + ); + }); + } catch (e) { + return onErr(e); } - - private allowCapabilities(allowed: string[], requested: string[]): void { - console.log(`Widget ${this.widget.id} is allowed capabilities:`, allowed); - - for (const c of allowed) this.allowedCapabilities.add(c); - const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); - this.allowedEvents.push(...allowedEvents); - - this.transport - .send(WidgetApiToWidgetAction.NotifyCapabilities, { - requested, - approved: Array.from(this.allowedCapabilities), - }) - .catch((e) => { - console.warn("non-fatal error notifying widget of approved capabilities:", e); - }) - .then(() => { - this.emit("capabilitiesNotified"); - }); - - // Push the initial room state for all rooms with a timeline capability - for (const c of allowed) { - if (isTimelineCapability(c)) { - const roomId = getTimelineRoomIDFromCapability(c); - if (roomId === Symbols.AnyRoom) { - for (const roomId of this.driver.getKnownRooms()) this.pushRoomState(roomId); - } else { - this.pushRoomState(roomId); - } - } - } - // If new events are allowed and the currently viewed room isn't covered - // by a timeline capability, then we know that there could be some state - // in the viewed room that the widget hasn't learned about yet- push it. - if (allowedEvents.length > 0 && this.viewedRoomId !== null && !this.canUseRoomTimeline(this.viewedRoomId)) { - this.pushRoomState(this.viewedRoomId); - } + } + + private handleOIDC(request: IGetOpenIDActionRequest): void { + let phase = 1; // 1 = initial request, 2 = after user manual confirmation + + const replyState = ( + state: OpenIDRequestState, + credential?: IOpenIDCredentials, + ): void | Promise => { + 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, + ): void | Promise => { + 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 handleReadRoomAccountData( + request: IReadRoomAccountDataFromWidgetActionRequest, + ): void | Promise { + let events: Promise = Promise.resolve([]); + events = this.driver.readRoomAccountData(request.data.type); + + if (!this.canReceiveRoomAccountData(request.data.type)) { + return this.transport.reply(request, { + error: { message: "Cannot read room account data of this type" }, + }); } - private onIframeLoad(ev: Event): void { - 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. - console.log("waitForIframeLoad is false: waiting for widget to send contentLoaded"); - this.contentLoadedWaitTimer = setTimeout(() => { - console.error( - "Widget specified waitForIframeLoad=false but timed out waiting for contentLoaded event!", - ); - }, 10000); - this.contentLoadedActionSent = false; - } + return events.then((evs) => { + this.transport.reply( + request, + { events: evs }, + ); + }); + } + + private async handleReadEvents( + request: IReadEventFromWidgetActionRequest, + ): Promise { + 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" }, + }); } - private handleContentLoadedAction(action: IContentLoadedActionRequest): void { - if (this.contentLoadedWaitTimer !== undefined) { - clearTimeout(this.contentLoadedWaitTimer); - this.contentLoadedWaitTimer = undefined; - } - if (this.contentLoadedActionSent) { - throw new Error( - "Improper sequence: ContentLoaded Action can only be sent 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 " + - "waitForIframeLoad is true (default=true)", - }, - }); - } else { - this.transport.reply(action, {}); - this.beginCapabilities(); + let askRoomIds: string[]; + if (request.data.room_ids === undefined) { + askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId]; + } else if (request.data.room_ids === Symbols.AnyRoom) { + askRoomIds = this.driver + .getKnownRooms() + .filter((roomId) => this.canUseRoomTimeline(roomId)); + } else { + askRoomIds = request.data.room_ids; + for (const roomId of askRoomIds) { + if (!this.canUseRoomTimeline(roomId)) { + return this.transport.reply(request, { + error: { message: `Unable to access room timeline: ${roomId}` }, + }); } - this.contentLoadedActionSent = true; + } } - private replyVersions(request: ISupportedVersionsActionRequest): void { - this.transport.reply(request, { - supported_versions: CurrentApiVersions, + const limit = request.data.limit || 0; + const since = request.data.since; + + let stateKey: string | undefined = undefined; + let msgtype: string | undefined = undefined; + if (request.data.state_key !== undefined) { + 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" }, + }); + } + } else { + msgtype = request.data.msgtype; + if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { + return this.transport.reply(request, { + error: { message: "Cannot read room events of this type" }, }); + } } - private handleCapabilitiesRenegotiate(request: IRenegotiateCapabilitiesActionRequest): void { - // 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 - skip validation - this.allowCapabilities([], []); - } - - this.driver - .validateCapabilities(newlyRequested) - .then((allowed) => this.allowCapabilities([...allowed], [...newlyRequested])); + // For backwards compatibility we still call the deprecated + // readRoomEvents and readStateEvents methods in case the client isn't + // letting us know the currently viewed room via setViewedRoomId + const events = + request.data.room_ids === undefined && askRoomIds.length === 0 + ? await (request.data.state_key === undefined + ? this.driver.readRoomEvents( + request.data.type, + msgtype, + limit, + null, + since, + ) + : this.driver.readStateEvents( + request.data.type, + stateKey, + limit, + null, + )) + : ( + await Promise.all( + askRoomIds.map((roomId) => + this.driver.readRoomTimeline( + roomId, + request.data.type, + msgtype, + stateKey, + limit, + since, + ), + ), + ) + ).flat(1); + this.transport.reply(request, { events }); + } + + private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { + if (!request.data.type) { + return this.transport.reply(request, { + error: { message: "Invalid request - missing event type" }, + }); } - private handleNavigate(request: INavigateActionRequest): void { - 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" }, - }); - } + 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 onErr = (e: unknown): void => { - console.error("[ClientWidgetApi] Failed to handle navigation: ", e); - this.handleDriverError(e, request, "Error handling navigation"); - }; - - try { - this.driver - .navigate(request.data.uri.toString()) - .catch((e: unknown) => onErr(e)) - .then(() => { - return this.transport.reply(request, {}); - }); - } catch (e) { - return onErr(e); - } + const isDelayedEvent = + request.data.delay !== undefined || + request.data.parent_delay_id !== undefined; + if ( + isDelayedEvent && + !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent) + ) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - private handleOIDC(request: IGetOpenIDActionRequest): void { - let phase = 1; // 1 = initial request, 2 = after user manual confirmation - - const replyState = ( - state: OpenIDRequestState, - credential?: IOpenIDCredentials, - ): void | Promise => { - 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): void | Promise => { - 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); + let sendEventPromise: Promise; + if (request.data.state_key !== undefined) { + if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { + return this.transport.reply(request, { + error: { message: "Cannot send state events of this type" }, }); - - this.driver.askOpenID(observer); + } + + if (!isDelayedEvent) { + sendEventPromise = this.driver.sendEvent( + request.data.type, + request.data.content || {}, + request.data.state_key, + request.data.room_id, + ); + } else { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, + 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" }, + }); + } + + if (!isDelayedEvent) { + sendEventPromise = this.driver.sendEvent( + request.data.type, + content, + null, // not sending a state event + request.data.room_id, + ); + } else { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, + request.data.type, + content, + null, // not sending a state event + request.data.room_id, + ); + } } - private handleReadRoomAccountData(request: IReadRoomAccountDataFromWidgetActionRequest): void | Promise { - let events: Promise = Promise.resolve([]); - events = this.driver.readRoomAccountData(request.data.type); - - if (!this.canReceiveRoomAccountData(request.data.type)) { - return this.transport.reply(request, { - error: { message: "Cannot read room account data of this type" }, - }); - } - return events.then((evs) => { - this.transport.reply(request, { events: evs }); + sendEventPromise + .then((sentEvent) => { + return this.transport.reply(request, { + room_id: sentEvent.roomId, + ...("eventId" in sentEvent + ? { + event_id: sentEvent.eventId, + } + : { + delay_id: sentEvent.delayId, + }), }); + }) + .catch((e: unknown) => { + console.error("error sending event: ", e); + this.handleDriverError(e, request, "Error sending event"); + }); + } + + private handleUpdateDelayedEvent( + request: IUpdateDelayedEventFromWidgetActionRequest, + ): void { + if (!request.data.delay_id) { + return this.transport.reply(request, { + error: { message: "Invalid request - missing delay_id" }, + }); } - private async handleReadEvents(request: IReadEventFromWidgetActionRequest): Promise { - 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[]; - if (request.data.room_ids === undefined) { - askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId]; - } else if (request.data.room_ids === Symbols.AnyRoom) { - askRoomIds = this.driver.getKnownRooms().filter((roomId) => this.canUseRoomTimeline(roomId)); - } else { - askRoomIds = request.data.room_ids; - 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; - const since = request.data.since; - - let stateKey: string | undefined = undefined; - let msgtype: string | undefined = undefined; - if (request.data.state_key !== undefined) { - 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" }, - }); - } - } else { - msgtype = request.data.msgtype; - if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { - return this.transport.reply(request, { - error: { message: "Cannot read room events of this type" }, - }); - } - } - - // For backwards compatibility we still call the deprecated - // readRoomEvents and readStateEvents methods in case the client isn't - // letting us know the currently viewed room via setViewedRoomId - const events = - request.data.room_ids === undefined && askRoomIds.length === 0 - ? await (request.data.state_key === undefined - ? this.driver.readRoomEvents(request.data.type, msgtype, limit, null, since) - : this.driver.readStateEvents(request.data.type, stateKey, limit, null)) - : ( - await Promise.all( - askRoomIds.map((roomId) => - this.driver.readRoomTimeline(roomId, request.data.type, msgtype, stateKey, limit, since), - ), - ) - ).flat(1); - this.transport.reply(request, { events }); + if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { - 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 isDelayedEvent = request.data.delay !== undefined || request.data.parent_delay_id !== undefined; - if (isDelayedEvent && !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); - } - - let sendEventPromise: Promise; - if (request.data.state_key !== undefined) { - if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { - return this.transport.reply(request, { - error: { message: "Cannot send state events of this type" }, - }); - } - - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( - request.data.type, - request.data.content || {}, - request.data.state_key, - request.data.room_id, - ); - } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, - 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" }, - }); - } - - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( - request.data.type, - content, - null, // not sending a state event - request.data.room_id, - ); - } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, - 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, - ...("eventId" in sentEvent - ? { - event_id: sentEvent.eventId, - } - : { - delay_id: sentEvent.delayId, - }), - }); - }) - .catch((e: unknown) => { - console.error("error sending event: ", e); - this.handleDriverError(e, request, "Error sending event"); - }); + switch (request.data.action) { + case UpdateDelayedEventAction.Cancel: + case UpdateDelayedEventAction.Restart: + case UpdateDelayedEventAction.Send: + this.driver + .updateDelayedEvent(request.data.delay_id, request.data.action) + .then(() => { + return this.transport.reply( + request, + {}, + ); + }) + .catch((e: unknown) => { + console.error("error updating delayed event: ", e); + this.handleDriverError(e, request, "Error updating delayed event"); + }); + break; + default: + return this.transport.reply(request, { + error: { message: "Invalid request - unsupported action" }, + }); } - - private handleUpdateDelayedEvent(request: IUpdateDelayedEventFromWidgetActionRequest): void { - if (!request.data.delay_id) { - return this.transport.reply(request, { - error: { message: "Invalid request - missing delay_id" }, - }); - } - - if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); - } - - switch (request.data.action) { - case UpdateDelayedEventAction.Cancel: - case UpdateDelayedEventAction.Restart: - case UpdateDelayedEventAction.Send: - this.driver - .updateDelayedEvent(request.data.delay_id, request.data.action) - .then(() => { - return this.transport.reply(request, {}); - }) - .catch((e: unknown) => { - console.error("error updating delayed event: ", e); - this.handleDriverError(e, request, "Error updating delayed event"); - }); - break; - default: - return this.transport.reply(request, { - error: { message: "Invalid request - unsupported action" }, - }); - } + } + + 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); + this.handleDriverError(e, request, "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); - this.handleDriverError(e, request, "Error sending event"); - } - } + } + + private async pollTurnServers( + turnServers: AsyncGenerator, + initialServer: ITurnServer, + ): Promise { + 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, + {}, + ); - private async pollTurnServers(turnServers: AsyncGenerator, initialServer: ITurnServer): Promise { - 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); - } + // 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 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 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, + ): Promise { + if (!request.data.event_id) { + return this.transport.reply(request, { + error: { message: "Invalid request - missing event ID" }, + }); } - private async handleReadRelations(request: IReadRelationsFromWidgetActionRequest): Promise { - 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); - this.handleDriverError(e, request, "Unexpected error while reading relations"); - } + if (request.data.limit !== undefined && request.data.limit < 0) { + return this.transport.reply(request, { + error: { message: "Invalid request - limit out of range" }, + }); } - private async handleUserDirectorySearch(request: IUserDirectorySearchFromWidgetActionRequest): Promise { - 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); - this.handleDriverError(e, request, "Unexpected error while searching in the user directory"); - } + 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}`, + }, + }); } - private async handleGetMediaConfig(request: IGetMediaConfigActionFromWidgetActionRequest): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); - } - - try { - const result = await this.driver.getMediaConfig(); - - return this.transport.reply(request, result); - } catch (e) { - console.error("error while getting the media configuration", e); - this.handleDriverError(e, request, "Unexpected error while getting the media configuration"); + 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); + this.handleDriverError( + e, + request, + "Unexpected error while reading relations", + ); } - - private async handleUploadFile(request: IUploadFileActionFromWidgetActionRequest): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); - } - - try { - const result = await this.driver.uploadFile(request.data.file); - - return this.transport.reply(request, { - content_uri: result.contentUri, - }); - } catch (e) { - console.error("error while uploading a file", e); - this.handleDriverError(e, request, "Unexpected error while uploading a file"); - } + } + + private async handleUserDirectorySearch( + request: IUserDirectorySearchFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC3973UserDirectorySearch)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - private async handleDownloadFile(request: IDownloadFileActionFromWidgetActionRequest): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039DownloadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); - } - - try { - const result = await this.driver.downloadFile(request.data.content_uri); - - return this.transport.reply(request, { file: result.file }); - } catch (e) { - console.error("error while downloading a file", e); - this.handleDriverError(e, request, "Unexpected error while downloading a file"); - } + if (typeof request.data.search_term !== "string") { + return this.transport.reply(request, { + error: { message: "Invalid request - missing search term" }, + }); } - private handleDriverError(e: unknown, request: IWidgetApiRequest, message: string): void { - const data = this.driver.processError(e); - this.transport.reply(request, { - error: { - message, - ...data, - }, - }); + if (request.data.limit !== undefined && request.data.limit < 0) { + return this.transport.reply(request, { + error: { message: "Invalid request - limit out of range" }, + }); } - private handleMessage(ev: CustomEvent): void | Promise { - 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); - case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: - return this.handleReadRoomAccountData(ev.detail); - case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: - return this.handleGetMediaConfig(ev.detail); - case WidgetApiFromWidgetAction.MSC4039UploadFileAction: - return this.handleUploadFile(ev.detail); - case WidgetApiFromWidgetAction.MSC4039DownloadFileAction: - return this.handleDownloadFile(ev.detail); - case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent: - return this.handleUpdateDelayedEvent(ev.detail); - - default: - return this.transport.reply(ev.detail, { - error: { - message: "Unknown or unsupported action: " + ev.detail.action, - }, - }); - } - } + 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); + this.handleDriverError( + e, + request, + "Unexpected error while searching in the user directory", + ); } - - /** - * Informs the widget that the client's theme has changed. - * @param theme The theme data, as an object with arbitrary contents. - */ - public updateTheme(theme: IThemeChangeActionRequestData): Promise { - return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme); + } + + private async handleGetMediaConfig( + request: IGetMediaConfigActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - /** - * Informs the widget that the client's language has changed. - * @param lang The BCP 47 identifier representing the client's current language. - */ - public updateLanguage(lang: string): Promise { - return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { lang }); + try { + const result = await this.driver.getMediaConfig(); + + return this.transport.reply( + request, + result, + ); + } catch (e) { + console.error("error while getting the media configuration", e); + this.handleDriverError( + e, + request, + "Unexpected error while getting the media configuration", + ); } - - /** - * 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, {}); + } + + private async handleUploadFile( + request: IUploadFileActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - /** - * 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, - }); + try { + const result = await this.driver.uploadFile(request.data.file); + + return this.transport.reply( + request, + { + content_uri: result.contentUri, + }, + ); + } catch (e) { + console.error("error while uploading a file", e); + this.handleDriverError( + e, + request, + "Unexpected error while uploading a file", + ); } - - public sendWidgetConfig(data: IModalWidgetOpenRequestData): Promise { - return this.transport.send(WidgetApiToWidgetAction.WidgetConfig, data).then(); + } + + private async handleDownloadFile( + request: IDownloadFileActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039DownloadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }); } - public notifyModalWidgetButtonClicked(id: IModalWidgetOpenRequestDataButton["id"]): Promise { - return this.transport - .send(WidgetApiToWidgetAction.ButtonClicked, { id }) - .then(); + try { + const result = await this.driver.downloadFile(request.data.content_uri); + + return this.transport.reply( + request, + { file: result.file }, + ); + } catch (e) { + console.error("error while downloading a file", e); + this.handleDriverError( + e, + request, + "Unexpected error while downloading a file", + ); } - - public notifyModalWidgetClose(data: IModalWidgetReturnData): Promise { - return this.transport.send(WidgetApiToWidgetAction.CloseModalWidget, data).then(); + } + + private handleDriverError( + e: unknown, + request: IWidgetApiRequest, + message: string, + ): void { + const data = this.driver.processError(e); + this.transport.reply(request, { + error: { + message, + ...data, + }, + }); + } + + private handleMessage( + ev: CustomEvent, + ): void | Promise { + 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, + ); + case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: + return this.handleReadRoomAccountData( + ev.detail, + ); + case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: + return this.handleGetMediaConfig( + ev.detail, + ); + case WidgetApiFromWidgetAction.MSC4039UploadFileAction: + return this.handleUploadFile( + ev.detail, + ); + case WidgetApiFromWidgetAction.MSC4039DownloadFileAction: + return this.handleDownloadFile( + ev.detail, + ); + case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent: + return this.handleUpdateDelayedEvent( + ev.detail, + ); + + default: + return this.transport.reply(ev.detail, { + error: { + message: "Unknown or unsupported action: " + ev.detail.action, + }, + }); + } } - - /** - * Feeds an event to the widget. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. - * @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 delivered or if the widget is not - * able to read the event due to permissions, rejects if the widget failed - * to handle the event. - * @deprecated It is recommended to communicate the viewed room ID by calling - * {@link ClientWidgetApi.setViewedRoomId} rather than passing it to this - * method. - */ - public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise; - /** - * Feeds an event to the widget. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. - * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. - * @returns {Promise} Resolves when delivered or if the widget is not - * able to read the event due to permissions, rejects if the widget failed - * to handle the event. - */ - public async feedEvent(rawEvent: IRoomEvent): Promise; - public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId?: string): Promise { - if (currentViewedRoomId !== undefined) this.setViewedRoomId(currentViewedRoomId); - if (rawEvent.room_id !== this.viewedRoomId && !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, - // it's compatible, but missing the index signature - rawEvent as ISendEventToWidgetRequestData, - ); + } + + /** + * Informs the widget that the client's theme has changed. + * @param theme The theme data, as an object with arbitrary contents. + */ + public updateTheme( + theme: IThemeChangeActionRequestData, + ): Promise { + return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme); + } + + /** + * Informs the widget that the client's language has changed. + * @param lang The BCP 47 identifier representing the client's current language. + */ + public updateLanguage(lang: string): Promise { + return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { + lang, + }); + } + + /** + * 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, < + IVisibilityActionRequestData + >{ + 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. As a client you are expected to call this + * for every new event in every room to which you are joined or invited. + * @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 delivered or if the widget is not + * able to read the event due to permissions, rejects if the widget failed + * to handle the event. + * @deprecated It is recommended to communicate the viewed room ID by calling + * {@link ClientWidgetApi.setViewedRoomId} rather than passing it to this + * method. + */ + public async feedEvent( + rawEvent: IRoomEvent, + currentViewedRoomId: string, + ): Promise; + /** + * Feeds an event to the widget. As a client you are expected to call this + * for every new event in every room to which you are joined or invited. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @returns {Promise} Resolves when delivered or if the widget is not + * able to read the event due to permissions, rejects if the widget failed + * to handle the event. + */ + public async feedEvent(rawEvent: IRoomEvent): Promise; + public async feedEvent( + rawEvent: IRoomEvent, + currentViewedRoomId?: string, + ): Promise { + if (currentViewedRoomId !== undefined) + this.setViewedRoomId(currentViewedRoomId); + if ( + rawEvent.room_id !== this.viewedRoomId && + !this.canUseRoomTimeline(rawEvent.room_id) + ) { + return; // no-op } - /** - * Feeds a to-device event to the widget. As a client you are expected to - * call this for every to-device event you receive. - * @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 delivered or if the widget is not - * able to receive the event due to permissions, rejects if the widget - * failed to handle the event. - */ - 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, - ); - } + 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 + } } - private viewedRoomId: string | null = null; - - /** - * Indicate that a room is being viewed (making it possible for the widget - * to interact with it). - */ - public setViewedRoomId(roomId: string | null): void { - this.viewedRoomId = roomId; - // If the widget doesn't have timeline permissions for the room then - // this is its opportunity to learn the room state. We push the entire - // room state, which could be redundant if this room had been viewed - // once before, but it's easier than selectively pushing just the bits - // of state that changed while the room was in the background. - if (roomId !== null && !this.canUseRoomTimeline(roomId)) this.pushRoomState(roomId); + // Feed the event into the widget + await this.transport.send( + WidgetApiToWidgetAction.SendEvent, + // it's compatible, but missing the index signature + rawEvent as ISendEventToWidgetRequestData, + ); + } + + /** + * Feeds a to-device event to the widget. As a client you are expected to + * call this for every to-device event you receive. + * @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 delivered or if the widget is not + * able to receive the event due to permissions, rejects if the widget + * failed to handle the event. + */ + 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, + ); } - - private async flushRoomState(): Promise { - try { - // Only send a single action once all concurrent tasks have completed - do await Promise.all([...this.pushRoomStateTasks]); - while (this.pushRoomStateTasks.size > 0); - - const events: IRoomEvent[] = []; - for (const eventTypeMap of this.pushRoomStateResult.values()) { - for (const stateKeyMap of eventTypeMap.values()) { - events.push(...stateKeyMap.values()); - } - } - await this.transport.send(WidgetApiToWidgetAction.UpdateState, { - state: events, - }); - } finally { - this.flushRoomStateTask = null; + } + + private viewedRoomId: string | null = null; + + /** + * Indicate that a room is being viewed (making it possible for the widget + * to interact with it). + */ + public setViewedRoomId(roomId: string | null): void { + this.viewedRoomId = roomId; + // If the widget doesn't have timeline permissions for the room then + // this is its opportunity to learn the room state. We push the entire + // room state, which could be redundant if this room had been viewed + // once before, but it's easier than selectively pushing just the bits + // of state that changed while the room was in the background. + if (roomId !== null && !this.canUseRoomTimeline(roomId)) + this.pushRoomState(roomId); + } + + private async flushRoomState(): Promise { + try { + // Only send a single action once all concurrent tasks have completed + do await Promise.all([...this.pushRoomStateTasks]); + while (this.pushRoomStateTasks.size > 0); + + const events: IRoomEvent[] = []; + for (const eventTypeMap of this.pushRoomStateResult.values()) { + for (const stateKeyMap of eventTypeMap.values()) { + events.push(...stateKeyMap.values()); } + } + await this.transport.send( + WidgetApiToWidgetAction.UpdateState, + { + state: events, + }, + ); + } finally { + this.flushRoomStateTask = null; } - - /** - * Read the room's state and push all entries that the widget is allowed to - * read through to the widget. - */ - private pushRoomState(roomId: string): void { - for (const cap of this.allowedEvents) { - if (cap.kind === EventKind.State && cap.direction === EventDirection.Receive) { - // Initiate the task - const events = this.driver.readRoomState(roomId, cap.eventType, cap.keyStr ?? undefined); - const task = events - .then( - (events) => { - // When complete, queue the resulting events to be - // pushed to the widget - for (const event of events) { - let eventTypeMap = this.pushRoomStateResult.get(roomId); - if (eventTypeMap === undefined) { - eventTypeMap = new Map(); - this.pushRoomStateResult.set(roomId, eventTypeMap); - } - let stateKeyMap = eventTypeMap.get(cap.eventType); - if (stateKeyMap === undefined) { - stateKeyMap = new Map(); - eventTypeMap.set(cap.eventType, stateKeyMap); - } - if (!stateKeyMap.has(event.state_key!)) stateKeyMap.set(event.state_key!, event); - } - }, - (e) => - console.error( - `Failed to read room state for ${roomId} (${cap.eventType}, ${cap.keyStr})`, - e, - ), - ) - .then(() => { - // Mark request as no longer pending - this.pushRoomStateTasks.delete(task); - }); - - // Mark task as pending - this.pushRoomStateTasks.add(task); - // Assuming no other tasks are already happening concurrently, - // schedule the widget action that actually pushes the events - this.flushRoomStateTask ??= this.flushRoomState(); - this.flushRoomStateTask.catch((e) => console.error("Failed to push room state", e)); - } - } + } + + /** + * Read the room's state and push all entries that the widget is allowed to + * read through to the widget. + */ + private pushRoomState(roomId: string): void { + for (const cap of this.allowedEvents) { + if ( + cap.kind === EventKind.State && + cap.direction === EventDirection.Receive + ) { + // Initiate the task + const events = this.driver.readRoomState( + roomId, + cap.eventType, + cap.keyStr ?? undefined, + ); + const task = events + .then( + (events) => { + // When complete, queue the resulting events to be + // pushed to the widget + for (const event of events) { + let eventTypeMap = this.pushRoomStateResult.get(roomId); + if (eventTypeMap === undefined) { + eventTypeMap = new Map(); + this.pushRoomStateResult.set(roomId, eventTypeMap); + } + let stateKeyMap = eventTypeMap.get(cap.eventType); + if (stateKeyMap === undefined) { + stateKeyMap = new Map(); + eventTypeMap.set(cap.eventType, stateKeyMap); + } + if (!stateKeyMap.has(event.state_key!)) + stateKeyMap.set(event.state_key!, event); + } + }, + (e) => + console.error( + `Failed to read room state for ${roomId} (${cap.eventType}, ${cap.keyStr})`, + e, + ), + ) + .then(() => { + // Mark request as no longer pending + this.pushRoomStateTasks.delete(task); + }); + + // Mark task as pending + this.pushRoomStateTasks.add(task); + // Assuming no other tasks are already happening concurrently, + // schedule the widget action that actually pushes the events + this.flushRoomStateTask ??= this.flushRoomState(); + this.flushRoomStateTask.catch((e) => + console.error("Failed to push room state", e), + ); + } } + } - /** + /** * Feeds a room state update to the widget. As a client you are expected to * call this for every state update in every room to which you are joined or * invited. @@ -1162,38 +1470,43 @@ export class ClientWidgetApi extends EventEmitter { * able to receive the room state due to permissions, rejects if the widget failed to handle the update. */ - public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) throw new Error("Not a state event"); - if ( - (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) && - this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) - ) { - // Updates could race with the initial push of the room's state - if (this.pushRoomStateTasks.size === 0) { - // No initial push tasks are pending; safe to send immediately - await this.transport.send(WidgetApiToWidgetAction.UpdateState, { - state: [rawEvent], - }); - } else { - // Lump the update in with whatever data will be sent in the - // initial push later. Even if we set it to an "outdated" entry - // here, we can count on any newer entries being passed to this - // same method eventually; this won't cause stuck state. - let eventTypeMap = this.pushRoomStateResult.get(rawEvent.room_id); - if (eventTypeMap === undefined) { - eventTypeMap = new Map(); - this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap); - } - let stateKeyMap = eventTypeMap.get(rawEvent.type); - if (stateKeyMap === undefined) { - stateKeyMap = new Map(); - eventTypeMap.set(rawEvent.type, stateKeyMap); - } - if (!stateKeyMap.has(rawEvent.type)) stateKeyMap.set(rawEvent.state_key, rawEvent); - do await Promise.all([...this.pushRoomStateTasks]); - while (this.pushRoomStateTasks.size > 0); - await this.flushRoomStateTask; - } + public async feedStateUpdate(rawEvent: IRoomEvent): Promise { + if (rawEvent.state_key === undefined) throw new Error("Not a state event"); + if ( + (rawEvent.room_id === this.viewedRoomId || + this.canUseRoomTimeline(rawEvent.room_id)) && + this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) + ) { + // Updates could race with the initial push of the room's state + if (this.pushRoomStateTasks.size === 0) { + // No initial push tasks are pending; safe to send immediately + await this.transport.send( + WidgetApiToWidgetAction.UpdateState, + { + state: [rawEvent], + }, + ); + } else { + // Lump the update in with whatever data will be sent in the + // initial push later. Even if we set it to an "outdated" entry + // here, we can count on any newer entries being passed to this + // same method eventually; this won't cause stuck state. + let eventTypeMap = this.pushRoomStateResult.get(rawEvent.room_id); + if (eventTypeMap === undefined) { + eventTypeMap = new Map(); + this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap); + } + let stateKeyMap = eventTypeMap.get(rawEvent.type); + if (stateKeyMap === undefined) { + stateKeyMap = new Map(); + eventTypeMap.set(rawEvent.type, stateKeyMap); } + if (!stateKeyMap.has(rawEvent.type)) + stateKeyMap.set(rawEvent.state_key, rawEvent); + do await Promise.all([...this.pushRoomStateTasks]); + while (this.pushRoomStateTasks.size > 0); + await this.flushRoomStateTask; + } } + } } diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index df92c03..c69767e 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -15,50 +15,50 @@ */ import { - Capability, - IOpenIDCredentials, - OpenIDRequestState, - SimpleObservable, - IRoomEvent, - IRoomAccountData, - ITurnServer, - IWidgetApiErrorResponseDataDetails, - UpdateDelayedEventAction, + Capability, + IOpenIDCredentials, + OpenIDRequestState, + SimpleObservable, + IRoomEvent, + IRoomAccountData, + ITurnServer, + IWidgetApiErrorResponseDataDetails, + UpdateDelayedEventAction, } from ".."; export interface ISendEventDetails { - roomId: string; - eventId: string; + roomId: string; + eventId: string; } export interface ISendDelayedEventDetails { - roomId: string; - delayId: string; + roomId: string; + delayId: string; } export interface IOpenIDUpdate { - state: OpenIDRequestState; - token?: IOpenIDCredentials; + state: OpenIDRequestState; + token?: IOpenIDCredentials; } export interface IReadEventRelationsResult { - chunk: IRoomEvent[]; - nextBatch?: string; - prevBatch?: string; + chunk: IRoomEvent[]; + nextBatch?: string; + prevBatch?: string; } export interface ISearchUserDirectoryResult { - limited: boolean; - results: Array<{ - userId: string; - displayName?: string; - avatarUrl?: string; - }>; + limited: boolean; + results: Array<{ + userId: string; + displayName?: string; + avatarUrl?: string; + }>; } export interface IGetMediaConfigResult { - [key: string]: unknown; - "m.upload.size"?: number; + [key: string]: unknown; + "m.upload.size"?: number; } /** @@ -71,344 +71,366 @@ export interface IGetMediaConfigResult { * instance already. */ export abstract class WidgetDriver { - /** - * Verifies the widget's requested capabilities, returning the ones - * it is approved to use. Mutating the requested capabilities will - * have no effect. - * - * This SHOULD result in the user being prompted to approve/deny - * capabilities. - * - * By default this rejects all capabilities (returns an empty set). - * @param {Set} requested The set of requested capabilities. - * @returns {Promise>} Resolves to the allowed capabilities. - */ - public validateCapabilities(requested: Set): Promise> { - return Promise.resolve(new Set()); - } + /** + * Verifies the widget's requested capabilities, returning the ones + * it is approved to use. Mutating the requested capabilities will + * have no effect. + * + * This SHOULD result in the user being prompted to approve/deny + * capabilities. + * + * By default this rejects all capabilities (returns an empty set). + * @param {Set} requested The set of requested capabilities. + * @returns {Promise>} Resolves to the allowed capabilities. + */ + public validateCapabilities( + requested: Set, + ): Promise> { + return Promise.resolve(new Set()); + } - /** - * Sends an event into a room. If `roomId` is falsy, the client should send the event - * into the room the user is currently looking at. The widget API will have already - * verified that the widget is capable of sending the event to that room. - * @param {string} eventType The event type to be sent. - * @param {*} content The content for the event. - * @param {string|null} stateKey The state key if this is a state event, otherwise null. - * May be an empty string. - * @param {string|null} roomId The room ID to send the event to. If falsy, the room the - * user is currently looking at. - * @returns {Promise} Resolves when the event has been sent with - * details of that event. - * @throws Rejected when the event could not be sent. - */ - public sendEvent( - eventType: string, - content: unknown, - stateKey: string | null = null, - roomId: string | null = null, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * Sends an event into a room. If `roomId` is falsy, the client should send the event + * into the room the user is currently looking at. The widget API will have already + * verified that the widget is capable of sending the event to that room. + * @param {string} eventType The event type to be sent. + * @param {*} content The content for the event. + * @param {string|null} stateKey The state key if this is a state event, otherwise null. + * May be an empty string. + * @param {string|null} roomId The room ID to send the event to. If falsy, the room the + * user is currently looking at. + * @returns {Promise} Resolves when the event has been sent with + * details of that event. + * @throws Rejected when the event could not be sent. + */ + public sendEvent( + eventType: string, + content: unknown, + stateKey: string | null = null, + roomId: string | null = null, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } - /** - * @experimental Part of MSC4140 & MSC4157 - * Sends a delayed event into a room. If `roomId` is falsy, the client should send it - * into the room the user is currently looking at. The widget API will have already - * verified that the widget is capable of sending the event to that room. - * @param {number|null} delay How much later to send the event, or null to not send the - * event automatically. May not be null if {@link parentDelayId} is null. - * @param {string|null} parentDelayId The ID of the delayed event this one is grouped with, - * or null if it will be put in a new group. May not be null if {@link delay} is null. - * @param {string} eventType The event type of the event to be sent. - * @param {*} content The content for the event to be sent. - * @param {string|null} stateKey The state key if the event to be sent a state event, - * otherwise null. May be an empty string. - * @param {string|null} roomId The room ID to send the event to. If falsy, the room the - * user is currently looking at. - * @returns {Promise} Resolves when the delayed event has been - * prepared with details of how to refer to it for updating/sending/canceling it later. - * @throws Rejected when the delayed event could not be sent. - */ - public sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, - eventType: string, - content: unknown, - stateKey: string | null = null, - roomId: string | null = null, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * @experimental Part of MSC4140 & MSC4157 + * Sends a delayed event into a room. If `roomId` is falsy, the client should send it + * into the room the user is currently looking at. The widget API will have already + * verified that the widget is capable of sending the event to that room. + * @param {number|null} delay How much later to send the event, or null to not send the + * event automatically. May not be null if {@link parentDelayId} is null. + * @param {string|null} parentDelayId The ID of the delayed event this one is grouped with, + * or null if it will be put in a new group. May not be null if {@link delay} is null. + * @param {string} eventType The event type of the event to be sent. + * @param {*} content The content for the event to be sent. + * @param {string|null} stateKey The state key if the event to be sent a state event, + * otherwise null. May be an empty string. + * @param {string|null} roomId The room ID to send the event to. If falsy, the room the + * user is currently looking at. + * @returns {Promise} Resolves when the delayed event has been + * prepared with details of how to refer to it for updating/sending/canceling it later. + * @throws Rejected when the delayed event could not be sent. + */ + public sendDelayedEvent( + delay: number | null, + parentDelayId: string | null, + eventType: string, + content: unknown, + stateKey: string | null = null, + roomId: string | null = null, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } - /** - * @experimental Part of MSC4140 & MSC4157 - * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. - * @throws Rejected when there is no matching delayed event, or when the action failed to run. - */ - public updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * @experimental Part of MSC4140 & MSC4157 + * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. + * @throws Rejected when there is no matching delayed event, or when the action failed to run. + */ + public updateDelayedEvent( + delayId: string, + action: UpdateDelayedEventAction, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } - /** - * Sends a to-device event. The widget API will have already verified that the widget - * is capable of sending the event. - * @param {string} eventType The event type to be sent. - * @param {boolean} encrypted Whether to encrypt the message contents. - * @param {Object} contentMap A map from user ID and device ID to event content. - * @returns {Promise} Resolves when the event has been sent. - * @throws Rejected when the event could not be sent. - */ - public sendToDevice( - eventType: string, - encrypted: boolean, - contentMap: { [userId: string]: { [deviceId: string]: object } }, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } - /** - * Reads an element of room account data. The widget API will have already verified that the widget is - * capable of receiving the `eventType` of the requested information. If `roomIds` is supplied, it may - * contain `Symbols.AnyRoom` to denote that the piece of room account data in each of the client's known - * rooms should be returned. When `null`, only the room the user is currently looking at should be considered. - * @param eventType The event type to be read. - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @returns {Promise} Resolves to the element of room account data, or an empty array. - */ - public readRoomAccountData(eventType: string, roomIds: string[] | null = null): Promise { - return Promise.resolve([]); - } + /** + * Sends a to-device event. The widget API will have already verified that the widget + * is capable of sending the event. + * @param {string} eventType The event type to be sent. + * @param {boolean} encrypted Whether to encrypt the message contents. + * @param {Object} contentMap A map from user ID and device ID to event content. + * @returns {Promise} Resolves when the event has been sent. + * @throws Rejected when the event could not be sent. + */ + public sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } + /** + * Reads an element of room account data. The widget API will have already verified that the widget is + * capable of receiving the `eventType` of the requested information. If `roomIds` is supplied, it may + * contain `Symbols.AnyRoom` to denote that the piece of room account data in each of the client's known + * rooms should be returned. When `null`, only the room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the element of room account data, or an empty array. + */ + public readRoomAccountData( + eventType: string, + roomIds: string[] | null = null, + ): Promise { + return Promise.resolve([]); + } - /** - * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that - * `limit` in each of the client's known rooms should be returned. When `null`, only the - * room the user is currently looking at should be considered. If `since` is specified but - * the event ID isn't present in the number of events fetched by the client due to `limit`, - * the client will return all the events. - * @param eventType The event type to be read. - * @param msgtype The msgtype of the events to be read, if applicable/defined. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many - * as possible". - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @param since When null, retrieves the number of events specified by the "limit" parameter. - * Otherwise, the event ID at which only subsequent events will be returned, as many as specified - * in "limit". - * @returns {Promise} Resolves to the room events, or an empty array. - * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. - */ - public readRoomEvents( - eventType: string, - msgtype: string | undefined, - limit: number, - roomIds: string[] | null = null, - since?: string, - ): Promise { - return Promise.resolve([]); - } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. If `since` is specified but + * the event ID isn't present in the number of events fetched by the client due to `limit`, + * the client will return all the events. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @param since When null, retrieves the number of events specified by the "limit" parameter. + * Otherwise, the event ID at which only subsequent events will be returned, as many as specified + * in "limit". + * @returns {Promise} Resolves to the room events, or an empty array. + * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. + */ + public readRoomEvents( + eventType: string, + msgtype: string | undefined, + limit: number, + roomIds: string[] | null = null, + since?: string, + ): Promise { + return Promise.resolve([]); + } - /** - * Reads all events of the given type, and optionally state key (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that - * `limit` in each of the client's known rooms should be returned. When `null`, only the - * room the user is currently looking at should be considered. - * @param eventType The event type to be read. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve. Will be zero to denote "as many - * as possible". - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @returns {Promise} Resolves to the state events, or an empty array. - * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. - */ - public readStateEvents( - eventType: string, - stateKey: string | undefined, - limit: number, - roomIds: string[] | null = null, - ): Promise { - return Promise.resolve([]); - } + /** + * Reads all events of the given type, and optionally state key (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the state events, or an empty array. + * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. + */ + public readStateEvents( + eventType: string, + stateKey: string | undefined, + limit: number, + roomIds: string[] | null = null, + ): Promise { + return Promise.resolve([]); + } - /** - * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. - * @param roomId The ID of the room to look within. - * @param eventType The event type to be read. - * @param msgtype The msgtype of the events to be read, if applicable/defined. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve. Will be zero to denote "as many as - * possible". - * @param since When null, retrieves the number of events specified by the "limit" parameter. - * Otherwise, the event ID at which only subsequent events will be returned, as many as specified - * in "limit". - * @returns {Promise} Resolves to the room events, or an empty array. - */ - public readRoomTimeline( - roomId: string, - eventType: string, - msgtype: string | undefined, - stateKey: string | undefined, - limit: number, - since: string | undefined, - ): Promise { - // For backward compatibility we try the deprecated methods, in case - // they're implemented - if (stateKey === undefined) return this.readRoomEvents(eventType, msgtype, limit, [roomId], since); - else return this.readStateEvents(eventType, stateKey, limit, [roomId]); - } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. + * @param roomId The ID of the room to look within. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many as + * possible". + * @param since When null, retrieves the number of events specified by the "limit" parameter. + * Otherwise, the event ID at which only subsequent events will be returned, as many as specified + * in "limit". + * @returns {Promise} Resolves to the room events, or an empty array. + */ + public readRoomTimeline( + roomId: string, + eventType: string, + msgtype: string | undefined, + stateKey: string | undefined, + limit: number, + since: string | undefined, + ): Promise { + // For backward compatibility we try the deprecated methods, in case + // they're implemented + if (stateKey === undefined) + return this.readRoomEvents(eventType, msgtype, limit, [roomId], since); + else return this.readStateEvents(eventType, stateKey, limit, [roomId]); + } - /** - * Reads the current values of all matching room state entries. - * @param roomId The ID of the room. - * @param eventType The event type of the entries to be read. - * @param stateKey The state key of the entry to be read. If undefined, - * all room state entries with a matching event type should be returned. - * @returns {Promise} Resolves to the events representing the - * current values of the room state entries. - */ - public readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise { - return Promise.resolve([]); - } + /** + * Reads the current values of all matching room state entries. + * @param roomId The ID of the room. + * @param eventType The event type of the entries to be read. + * @param stateKey The state key of the entry to be read. If undefined, + * all room state entries with a matching event type should be returned. + * @returns {Promise} Resolves to the events representing the + * current values of the room state entries. + */ + public readRoomState( + roomId: string, + eventType: string, + stateKey: string | undefined, + ): Promise { + return Promise.resolve([]); + } - /** - * Reads all events that are related to a given event. The widget API will - * have already verified that the widget is capable of receiving the event, - * or will make sure to reject access to events which are returned from this - * function, but are not capable of receiving. If `relationType` or `eventType` - * are set, the returned events should already be filtered. Less events than - * the limit are allowed to be returned, but not more. - * @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 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 limit The maximum number of events to retrieve per room. If not - * supplied, the server will apply a default limit. - * @param direction The direction to search for according to MSC3715 - * @returns Resolves to the room relations. - */ - public readEventRelations( - eventId: string, - roomId?: string, - relationType?: string, - eventType?: string, - from?: string, - to?: string, - limit?: number, - direction?: "f" | "b", - ): Promise { - return Promise.resolve({ chunk: [] }); - } + /** + * Reads all events that are related to a given event. The widget API will + * have already verified that the widget is capable of receiving the event, + * or will make sure to reject access to events which are returned from this + * function, but are not capable of receiving. If `relationType` or `eventType` + * are set, the returned events should already be filtered. Less events than + * the limit are allowed to be returned, but not more. + * @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 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 limit The maximum number of events to retrieve per room. If not + * supplied, the server will apply a default limit. + * @param direction The direction to search for according to MSC3715 + * @returns Resolves to the room relations. + */ + public readEventRelations( + eventId: string, + roomId?: string, + relationType?: string, + eventType?: string, + from?: string, + to?: string, + limit?: number, + direction?: "f" | "b", + ): Promise { + return Promise.resolve({ chunk: [] }); + } - /** - * Asks the user for permission to validate their identity through OpenID Connect. The - * interface for this function is an observable which accepts the state machine of the - * OIDC exchange flow. For example, if the client/user blocks the request then it would - * feed back a `{state: Blocked}` into the observable. Similarly, if the user already - * approved the widget then a `{state: Allowed}` would be fed into the observable alongside - * the token itself. If the client is asking for permission, it should feed in a - * `{state: PendingUserConfirmation}` followed by the relevant Allowed or Blocked state. - * - * The widget API will reject the widget's request with an error if this contract is not - * met properly. By default, the widget driver will block all OIDC requests. - * @param {SimpleObservable} observer The observable to feed updates into. - */ - public askOpenID(observer: SimpleObservable): void { - observer.update({ state: OpenIDRequestState.Blocked }); - } + /** + * Asks the user for permission to validate their identity through OpenID Connect. The + * interface for this function is an observable which accepts the state machine of the + * OIDC exchange flow. For example, if the client/user blocks the request then it would + * feed back a `{state: Blocked}` into the observable. Similarly, if the user already + * approved the widget then a `{state: Allowed}` would be fed into the observable alongside + * the token itself. If the client is asking for permission, it should feed in a + * `{state: PendingUserConfirmation}` followed by the relevant Allowed or Blocked state. + * + * The widget API will reject the widget's request with an error if this contract is not + * met properly. By default, the widget driver will block all OIDC requests. + * @param {SimpleObservable} observer The observable to feed updates into. + */ + public askOpenID(observer: SimpleObservable): void { + observer.update({ state: OpenIDRequestState.Blocked }); + } - /** - * Navigates the client with a matrix.to URI. In future this function will also be provided - * with the Matrix URIs once matrix.to is replaced. The given URI will have already been - * lightly checked to ensure it looks like a valid URI, though the implementation is recommended - * to do further checks on the URI. - * @param {string} uri The URI to navigate to. - * @returns {Promise} Resolves when complete. - * @throws Throws if there's a problem with the navigation, such as invalid format. - */ - public navigate(uri: string): Promise { - throw new Error("Navigation is not implemented"); - } + /** + * Navigates the client with a matrix.to URI. In future this function will also be provided + * with the Matrix URIs once matrix.to is replaced. The given URI will have already been + * lightly checked to ensure it looks like a valid URI, though the implementation is recommended + * to do further checks on the URI. + * @param {string} uri The URI to navigate to. + * @returns {Promise} Resolves when complete. + * @throws Throws if there's a problem with the navigation, such as invalid format. + */ + public navigate(uri: string): Promise { + throw new Error("Navigation is not implemented"); + } - /** - * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and - * thereafter yielding new credentials whenever the previous ones expire. The widget API will - * have already verified that the widget has permission to access TURN servers. - * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. - */ - public getTurnServers(): AsyncGenerator { - throw new Error("TURN server support is not implemented"); - } + /** + * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and + * thereafter yielding new credentials whenever the previous ones expire. The widget API will + * have already verified that the widget has permission to access TURN servers. + * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. + */ + public getTurnServers(): AsyncGenerator { + throw new Error("TURN server support is not implemented"); + } - /** - * 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 searchUserDirectory(searchTerm: string, limit?: number): Promise { - return Promise.resolve({ limited: false, results: [] }); - } + /** + * 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 searchUserDirectory( + searchTerm: string, + limit?: number, + ): Promise { + return Promise.resolve({ limited: false, results: [] }); + } - /** - * Get the config for the media repository. - * @returns Promise which resolves with an object containing the config. - */ - public getMediaConfig(): Promise { - throw new Error("Get media config is not implemented"); - } + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public getMediaConfig(): Promise { + throw new Error("Get media config is not implemented"); + } - /** - * Upload a file to the media repository on the homeserver. - * @param file - The object to upload. Something that can be sent to - * XMLHttpRequest.send (typically a File). - * @returns Resolves to the location of the uploaded file. - */ - public uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }> { - throw new Error("Upload file is not implemented"); - } + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public uploadFile( + file: XMLHttpRequestBodyInit, + ): Promise<{ contentUri: string }> { + throw new Error("Upload file is not implemented"); + } - /** - * Download a file from the media repository on the homeserver. - * @param contentUri - MXC URI of the file to download. - * @returns Resolves to the contents of the file. - */ - public downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }> { - throw new Error("Download file is not implemented"); - } + /** + * Download a file from the media repository on the homeserver. + * @param contentUri - MXC URI of the file to download. + * @returns Resolves to the contents of the file. + */ + public downloadFile( + contentUri: string, + ): Promise<{ file: XMLHttpRequestBodyInit }> { + throw new Error("Download file is not implemented"); + } - /** - * Gets the IDs of all joined or invited rooms currently known to the - * client. - * @returns The room IDs. - */ - public getKnownRooms(): string[] { - throw new Error("Querying known rooms is not implemented"); - } + /** + * Gets the IDs of all joined or invited rooms currently known to the + * client. + * @returns The room IDs. + */ + public getKnownRooms(): string[] { + throw new Error("Querying known rooms is not implemented"); + } - /** - * Expresses an error thrown by this driver in a format compatible with the Widget API. - * @param error The error to handle. - * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, - * or undefined if it cannot be expressed as one. - */ - public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined { - return undefined; - } + /** + * Expresses an error thrown by this driver in a format compatible with the Widget API. + * @param error The error to handle. + * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, + * or undefined if it cannot be expressed as one. + */ + public processError( + error: unknown, + ): IWidgetApiErrorResponseDataDetails | undefined { + return undefined; + } } diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index ab0546e..fa4dd8a 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -15,40 +15,40 @@ */ export enum MatrixApiVersion { - Prerelease1 = "0.0.1", - Prerelease2 = "0.0.2", - //V010 = "0.1.0", // first release + Prerelease1 = "0.0.1", + Prerelease2 = "0.0.2", + //V010 = "0.1.0", // first release } export enum UnstableApiVersion { - MSC2762 = "org.matrix.msc2762", - MSC2871 = "org.matrix.msc2871", - MSC2873 = "org.matrix.msc2873", - MSC2931 = "org.matrix.msc2931", - MSC2974 = "org.matrix.msc2974", - MSC2876 = "org.matrix.msc2876", - MSC3819 = "org.matrix.msc3819", - MSC3846 = "town.robin.msc3846", - MSC3869 = "org.matrix.msc3869", - MSC3973 = "org.matrix.msc3973", - MSC4039 = "org.matrix.msc4039", + MSC2762 = "org.matrix.msc2762", + MSC2871 = "org.matrix.msc2871", + MSC2873 = "org.matrix.msc2873", + MSC2931 = "org.matrix.msc2931", + MSC2974 = "org.matrix.msc2974", + MSC2876 = "org.matrix.msc2876", + MSC3819 = "org.matrix.msc3819", + MSC3846 = "town.robin.msc3846", + MSC3869 = "org.matrix.msc3869", + MSC3973 = "org.matrix.msc3973", + MSC4039 = "org.matrix.msc4039", } export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string; export const CurrentApiVersions: ApiVersion[] = [ - MatrixApiVersion.Prerelease1, - MatrixApiVersion.Prerelease2, - //MatrixApiVersion.V010, - UnstableApiVersion.MSC2762, - UnstableApiVersion.MSC2871, - UnstableApiVersion.MSC2873, - UnstableApiVersion.MSC2931, - UnstableApiVersion.MSC2974, - UnstableApiVersion.MSC2876, - UnstableApiVersion.MSC3819, - UnstableApiVersion.MSC3846, - UnstableApiVersion.MSC3869, - UnstableApiVersion.MSC3973, - UnstableApiVersion.MSC4039, + MatrixApiVersion.Prerelease1, + MatrixApiVersion.Prerelease2, + //MatrixApiVersion.V010, + UnstableApiVersion.MSC2762, + UnstableApiVersion.MSC2871, + UnstableApiVersion.MSC2873, + UnstableApiVersion.MSC2931, + UnstableApiVersion.MSC2974, + UnstableApiVersion.MSC2876, + UnstableApiVersion.MSC3819, + UnstableApiVersion.MSC3846, + UnstableApiVersion.MSC3869, + UnstableApiVersion.MSC3973, + UnstableApiVersion.MSC4039, ]; diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index f541ac5..1c0c1a6 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -17,45 +17,49 @@ import { Symbols } from "../Symbols"; export enum MatrixCapabilities { - Screenshots = "m.capability.screenshot", - StickerSending = "m.sticker", - AlwaysOnScreen = "m.always_on_screen", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - * Ask Element to not give the option to move the widget into a separate tab. - */ - RequiresClient = "io.element.requires_client", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2931Navigate = "org.matrix.msc2931.navigate", - MSC3846TurnServers = "town.robin.msc3846.turn_servers", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039UploadFile = "org.matrix.msc4039.upload_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039DownloadFile = "org.matrix.msc4039.download_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", + Screenshots = "m.capability.screenshot", + StickerSending = "m.sticker", + AlwaysOnScreen = "m.always_on_screen", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * Ask Element to not give the option to move the widget into a separate tab. + */ + RequiresClient = "io.element.requires_client", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2931Navigate = "org.matrix.msc2931.navigate", + MSC3846TurnServers = "town.robin.msc3846.turn_servers", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFile = "org.matrix.msc4039.upload_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039DownloadFile = "org.matrix.msc4039.download_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } export type Capability = MatrixCapabilities | string; -export const StickerpickerCapabilities: Capability[] = [MatrixCapabilities.StickerSending]; -export const VideoConferenceCapabilities: Capability[] = [MatrixCapabilities.AlwaysOnScreen]; +export const StickerpickerCapabilities: Capability[] = [ + MatrixCapabilities.StickerSending, +]; +export const VideoConferenceCapabilities: Capability[] = [ + MatrixCapabilities.AlwaysOnScreen, +]; /** * Determines if a capability is a capability for a timeline. @@ -63,8 +67,8 @@ export const VideoConferenceCapabilities: Capability[] = [MatrixCapabilities.Alw * @returns {boolean} True if a timeline capability, false otherwise. */ export function isTimelineCapability(capability: Capability): boolean { - // TODO: Change when MSC2762 becomes stable. - return capability?.startsWith("org.matrix.msc2762.timeline:"); + // TODO: Change when MSC2762 becomes stable. + return capability?.startsWith("org.matrix.msc2762.timeline:"); } /** @@ -73,8 +77,11 @@ export function isTimelineCapability(capability: Capability): boolean { * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` for that designation. * @returns {boolean} True if a matching capability, false otherwise. */ -export function isTimelineCapabilityFor(capability: Capability, roomId: string | Symbols.AnyRoom): boolean { - return capability === `org.matrix.msc2762.timeline:${roomId}`; +export function isTimelineCapabilityFor( + capability: Capability, + roomId: string | Symbols.AnyRoom, +): boolean { + return capability === `org.matrix.msc2762.timeline:${roomId}`; } /** @@ -82,6 +89,8 @@ export function isTimelineCapabilityFor(capability: Capability, roomId: string | * @param {string} capability The capability to parse. * @returns {string} The room ID. */ -export function getTimelineRoomIDFromCapability(capability: Capability): string { - return capability.substring(capability.indexOf(":") + 1); +export function getTimelineRoomIDFromCapability( + capability: Capability, +): string { + return capability.substring(capability.indexOf(":") + 1); } diff --git a/src/interfaces/CapabilitiesAction.ts b/src/interfaces/CapabilitiesAction.ts index 365bb79..cff440b 100644 --- a/src/interfaces/CapabilitiesAction.ts +++ b/src/interfaces/CapabilitiesAction.ts @@ -14,47 +14,64 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; import { Capability } from "./Capabilities"; -import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponseData, +} from "./IWidgetApiResponse"; export interface ICapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.Capabilities; - data: IWidgetApiRequestEmptyData; + action: WidgetApiToWidgetAction.Capabilities; + data: IWidgetApiRequestEmptyData; } -export interface ICapabilitiesActionResponseData extends IWidgetApiResponseData { - capabilities: Capability[]; +export interface ICapabilitiesActionResponseData + extends IWidgetApiResponseData { + capabilities: Capability[]; } -export interface ICapabilitiesActionResponse extends ICapabilitiesActionRequest { - response: ICapabilitiesActionResponseData; +export interface ICapabilitiesActionResponse + extends ICapabilitiesActionRequest { + response: ICapabilitiesActionResponseData; } -export interface INotifyCapabilitiesActionRequestData extends IWidgetApiRequestData { - requested: Capability[]; - approved: Capability[]; +export interface INotifyCapabilitiesActionRequestData + extends IWidgetApiRequestData { + requested: Capability[]; + approved: Capability[]; } export interface INotifyCapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.NotifyCapabilities; - data: INotifyCapabilitiesActionRequestData; + action: WidgetApiToWidgetAction.NotifyCapabilities; + data: INotifyCapabilitiesActionRequestData; } -export interface INotifyCapabilitiesActionResponse extends INotifyCapabilitiesActionRequest { - response: IWidgetApiAcknowledgeResponseData; +export interface INotifyCapabilitiesActionResponse + extends INotifyCapabilitiesActionRequest { + response: IWidgetApiAcknowledgeResponseData; } -export interface IRenegotiateCapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities; - data: IRenegotiateCapabilitiesRequestData; +export interface IRenegotiateCapabilitiesActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities; + data: IRenegotiateCapabilitiesRequestData; } -export interface IRenegotiateCapabilitiesRequestData extends IWidgetApiResponseData { - capabilities: Capability[]; +export interface IRenegotiateCapabilitiesRequestData + extends IWidgetApiResponseData { + capabilities: Capability[]; } -export interface IRenegotiateCapabilitiesActionResponse extends IRenegotiateCapabilitiesActionRequest { - // nothing +export interface IRenegotiateCapabilitiesActionResponse + extends IRenegotiateCapabilitiesActionRequest { + // nothing } diff --git a/src/interfaces/ContentLoadedAction.ts b/src/interfaces/ContentLoadedAction.ts index ceca93f..d5ae581 100644 --- a/src/interfaces/ContentLoadedAction.ts +++ b/src/interfaces/ContentLoadedAction.ts @@ -14,15 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IContentLoadedActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.ContentLoaded; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.ContentLoaded; + data: IWidgetApiRequestEmptyData; } -export interface IContentLoadedActionResponse extends IContentLoadedActionRequest { - response: IWidgetApiAcknowledgeResponseData; +export interface IContentLoadedActionResponse + extends IContentLoadedActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/DownloadFileAction.ts b/src/interfaces/DownloadFileAction.ts index f3eed2e..a678068 100644 --- a/src/interfaces/DownloadFileAction.ts +++ b/src/interfaces/DownloadFileAction.ts @@ -18,19 +18,23 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IDownloadFileActionFromWidgetRequestData extends IWidgetApiRequestData { - content_uri: string; // eslint-disable-line camelcase +export interface IDownloadFileActionFromWidgetRequestData + extends IWidgetApiRequestData { + content_uri: string; // eslint-disable-line camelcase } -export interface IDownloadFileActionFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; - data: IDownloadFileActionFromWidgetRequestData; +export interface IDownloadFileActionFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; + data: IDownloadFileActionFromWidgetRequestData; } -export interface IDownloadFileActionFromWidgetResponseData extends IWidgetApiResponseData { - file: XMLHttpRequestBodyInit; +export interface IDownloadFileActionFromWidgetResponseData + extends IWidgetApiResponseData { + file: XMLHttpRequestBodyInit; } -export interface IDownloadFileActionFromWidgetActionResponse extends IDownloadFileActionFromWidgetActionRequest { - response: IDownloadFileActionFromWidgetResponseData; +export interface IDownloadFileActionFromWidgetActionResponse + extends IDownloadFileActionFromWidgetActionRequest { + response: IDownloadFileActionFromWidgetResponseData; } diff --git a/src/interfaces/GetMediaConfigAction.ts b/src/interfaces/GetMediaConfigAction.ts index 71f19d1..f67c2c8 100644 --- a/src/interfaces/GetMediaConfigAction.ts +++ b/src/interfaces/GetMediaConfigAction.ts @@ -18,17 +18,21 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IGetMediaConfigActionFromWidgetRequestData extends IWidgetApiRequestData {} +export interface IGetMediaConfigActionFromWidgetRequestData + extends IWidgetApiRequestData {} -export interface IGetMediaConfigActionFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; - data: IGetMediaConfigActionFromWidgetRequestData; +export interface IGetMediaConfigActionFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; + data: IGetMediaConfigActionFromWidgetRequestData; } -export interface IGetMediaConfigActionFromWidgetResponseData extends IWidgetApiResponseData { - "m.upload.size"?: number; +export interface IGetMediaConfigActionFromWidgetResponseData + extends IWidgetApiResponseData { + "m.upload.size"?: number; } -export interface IGetMediaConfigActionFromWidgetActionResponse extends IGetMediaConfigActionFromWidgetActionRequest { - response: IGetMediaConfigActionFromWidgetResponseData; +export interface IGetMediaConfigActionFromWidgetActionResponse + extends IGetMediaConfigActionFromWidgetActionRequest { + response: IGetMediaConfigActionFromWidgetResponseData; } diff --git a/src/interfaces/GetOpenIDAction.ts b/src/interfaces/GetOpenIDAction.ts index 000313c..024829e 100644 --- a/src/interfaces/GetOpenIDAction.ts +++ b/src/interfaces/GetOpenIDAction.ts @@ -19,31 +19,33 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export enum OpenIDRequestState { - Allowed = "allowed", - Blocked = "blocked", - PendingUserConfirmation = "request", + Allowed = "allowed", + Blocked = "blocked", + PendingUserConfirmation = "request", } export interface IOpenIDCredentials { - access_token?: string; // eslint-disable-line camelcase - expires_in?: number; // eslint-disable-line camelcase - matrix_server_name?: string; // eslint-disable-line camelcase - token_type?: "Bearer" | string; // eslint-disable-line camelcase + access_token?: string; // eslint-disable-line camelcase + expires_in?: number; // eslint-disable-line camelcase + matrix_server_name?: string; // eslint-disable-line camelcase + token_type?: "Bearer" | string; // eslint-disable-line camelcase } export interface IGetOpenIDActionRequestData extends IWidgetApiRequestData { - // nothing + // nothing } export interface IGetOpenIDActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.GetOpenIDCredentials; - data: IGetOpenIDActionRequestData; + action: WidgetApiFromWidgetAction.GetOpenIDCredentials; + data: IGetOpenIDActionRequestData; } -export interface IGetOpenIDActionResponseData extends IWidgetApiResponseData, IOpenIDCredentials { - state: OpenIDRequestState; +export interface IGetOpenIDActionResponseData + extends IWidgetApiResponseData, + IOpenIDCredentials { + state: OpenIDRequestState; } export interface IGetOpenIDActionResponse extends IGetOpenIDActionRequest { - response: IGetOpenIDActionResponseData; + response: IGetOpenIDActionResponseData; } diff --git a/src/interfaces/ICustomWidgetData.ts b/src/interfaces/ICustomWidgetData.ts index 56657fb..b9360e5 100644 --- a/src/interfaces/ICustomWidgetData.ts +++ b/src/interfaces/ICustomWidgetData.ts @@ -20,8 +20,8 @@ import { IWidgetData } from "./IWidget"; * Widget data for m.custom specifically. */ export interface ICustomWidgetData extends IWidgetData { - /** - * The URL for the widget if the templated URL is not exactly what will be loaded. - */ - url?: string; + /** + * The URL for the widget if the templated URL is not exactly what will be loaded. + */ + url?: string; } diff --git a/src/interfaces/IJitsiWidgetData.ts b/src/interfaces/IJitsiWidgetData.ts index 65b22a0..414ba0d 100644 --- a/src/interfaces/IJitsiWidgetData.ts +++ b/src/interfaces/IJitsiWidgetData.ts @@ -20,19 +20,19 @@ import { IWidgetData } from "./IWidget"; * Widget data for m.jitsi widgets. */ export interface IJitsiWidgetData extends IWidgetData { - /** - * The domain where the Jitsi Meet conference is being held. - */ - domain: string; + /** + * The domain where the Jitsi Meet conference is being held. + */ + domain: string; - /** - * The conference ID (also known as the room name) where the conference is being held. - */ - conferenceId: string; + /** + * The conference ID (also known as the room name) where the conference is being held. + */ + conferenceId: string; - /** - * Optional. True to indicate that the conference should be without video, false - * otherwise (default). - */ - isAudioOnly?: boolean; + /** + * Optional. True to indicate that the conference should be without video, false + * otherwise (default). + */ + isAudioOnly?: boolean; } diff --git a/src/interfaces/IRoomAccountData.ts b/src/interfaces/IRoomAccountData.ts index 750bdef..f29a8ec 100644 --- a/src/interfaces/IRoomAccountData.ts +++ b/src/interfaces/IRoomAccountData.ts @@ -15,7 +15,7 @@ */ export interface IRoomAccountData { - type: string; - room_id: string; // eslint-disable-line camelcase - content: unknown; + type: string; + room_id: string; // eslint-disable-line camelcase + content: unknown; } diff --git a/src/interfaces/IRoomEvent.ts b/src/interfaces/IRoomEvent.ts index 5e90005..6df0336 100644 --- a/src/interfaces/IRoomEvent.ts +++ b/src/interfaces/IRoomEvent.ts @@ -15,12 +15,12 @@ */ export interface IRoomEvent { - type: string; - sender: string; - event_id: string; // eslint-disable-line camelcase - room_id: string; // eslint-disable-line camelcase - state_key?: string; // eslint-disable-line camelcase - origin_server_ts: number; // eslint-disable-line camelcase - content: unknown; - unsigned: unknown; + type: string; + sender: string; + event_id: string; // eslint-disable-line camelcase + room_id: string; // eslint-disable-line camelcase + state_key?: string; // eslint-disable-line camelcase + origin_server_ts: number; // eslint-disable-line camelcase + content: unknown; + unsigned: unknown; } diff --git a/src/interfaces/IStickerpickerWidgetData.ts b/src/interfaces/IStickerpickerWidgetData.ts index 1459fa5..816ca14 100644 --- a/src/interfaces/IStickerpickerWidgetData.ts +++ b/src/interfaces/IStickerpickerWidgetData.ts @@ -17,5 +17,5 @@ import { IWidgetData } from "./IWidget"; export interface IStickerpickerWidgetData extends IWidgetData { - // no additional properties (for now) + // no additional properties (for now) } diff --git a/src/interfaces/IWidget.ts b/src/interfaces/IWidget.ts index a6ee670..72893f4 100644 --- a/src/interfaces/IWidget.ts +++ b/src/interfaces/IWidget.ts @@ -20,15 +20,15 @@ import { WidgetType } from "./WidgetType"; * Widget data. */ export interface IWidgetData { - /** - * Optional title for the widget. - */ - title?: string; + /** + * Optional title for the widget. + */ + title?: string; - /** - * Custom keys for inclusion in the template URL. - */ - [key: string]: unknown; + /** + * Custom keys for inclusion in the template URL. + */ + [key: string]: unknown; } /** @@ -36,40 +36,40 @@ export interface IWidgetData { * https://matrix.org/docs/spec/widgets/latest#widgetcommonproperties-schema */ export interface IWidget { - /** - * The ID of the widget. - */ - id: string; + /** + * The ID of the widget. + */ + id: string; - /** - * The user ID who originally created the widget. - */ - creatorUserId: string; + /** + * The user ID who originally created the widget. + */ + creatorUserId: string; - /** - * Optional name for the widget. - */ - name?: string; + /** + * Optional name for the widget. + */ + name?: string; - /** - * The type of widget. - */ - type: WidgetType; + /** + * The type of widget. + */ + type: WidgetType; - /** - * The URL for the widget, with template variables. - */ - url: string; + /** + * The URL for the widget, with template variables. + */ + url: string; - /** - * Optional flag to indicate whether or not the client should initiate communication - * right after the iframe loads (default, true) or when the widget indicates it is - * ready (false). - */ - waitForIframeLoad?: boolean; + /** + * Optional flag to indicate whether or not the client should initiate communication + * right after the iframe loads (default, true) or when the widget indicates it is + * ready (false). + */ + waitForIframeLoad?: boolean; - /** - * Data for the widget. - */ - data?: IWidgetData; + /** + * Data for the widget. + */ + data?: IWidgetData; } diff --git a/src/interfaces/IWidgetApiErrorResponse.ts b/src/interfaces/IWidgetApiErrorResponse.ts index a215c2a..935ba6d 100644 --- a/src/interfaces/IWidgetApiErrorResponse.ts +++ b/src/interfaces/IWidgetApiErrorResponse.ts @@ -14,43 +14,53 @@ * limitations under the License. */ -import { IWidgetApiResponse, IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { + IWidgetApiResponse, + IWidgetApiResponseData, +} from "./IWidgetApiResponse"; /** * The format of errors returned by Matrix API requests * made by a WidgetDriver. */ export interface IMatrixApiError { - /** The HTTP status code of the associated request. */ - http_status: number; // eslint-disable-line camelcase - /** Any HTTP response headers that are relevant to the error. */ - http_headers: { [name: string]: string }; // eslint-disable-line camelcase - /** The URL of the failed request. */ - url: string; - /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ - response: { - errcode: string; - error: string; - } & IWidgetApiResponseData; // extensible + /** The HTTP status code of the associated request. */ + http_status: number; // eslint-disable-line camelcase + /** Any HTTP response headers that are relevant to the error. */ + http_headers: { [name: string]: string }; // eslint-disable-line camelcase + /** The URL of the failed request. */ + url: string; + /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ + response: { + errcode: string; + error: string; + } & IWidgetApiResponseData; // extensible } export interface IWidgetApiErrorResponseDataDetails { - /** Set if the error came from a Matrix API request made by a widget driver */ - matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase + /** Set if the error came from a Matrix API request made by a widget driver */ + matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase } export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData { - error: { - /** A user-friendly string describing the error */ - message: string; - } & IWidgetApiErrorResponseDataDetails; + error: { + /** A user-friendly string describing the error */ + message: string; + } & IWidgetApiErrorResponseDataDetails; } export interface IWidgetApiErrorResponse extends IWidgetApiResponse { - response: IWidgetApiErrorResponseData; + response: IWidgetApiErrorResponseData; } -export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData { - const error = responseData.error; - return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"; +export function isErrorResponse( + responseData: IWidgetApiResponseData, +): responseData is IWidgetApiErrorResponseData { + const error = responseData.error; + return ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ); } diff --git a/src/interfaces/IWidgetApiRequest.ts b/src/interfaces/IWidgetApiRequest.ts index f783630..4574035 100644 --- a/src/interfaces/IWidgetApiRequest.ts +++ b/src/interfaces/IWidgetApiRequest.ts @@ -18,21 +18,21 @@ import { WidgetApiDirection } from "./WidgetApiDirection"; import { WidgetApiAction } from "./WidgetApiAction"; export interface IWidgetApiRequestData { - [key: string]: unknown; + [key: string]: unknown; } export interface IWidgetApiRequestEmptyData extends IWidgetApiRequestData { - // nothing + // nothing } export interface IWidgetApiRequest { - api: WidgetApiDirection; - requestId: string; - action: WidgetApiAction; - widgetId: string; - data: IWidgetApiRequestData; - // XXX: This is for Scalar support - // TODO: Fix scalar - // eslint-disable-next-line @typescript-eslint/no-explicit-any - visible?: any; + api: WidgetApiDirection; + requestId: string; + action: WidgetApiAction; + widgetId: string; + data: IWidgetApiRequestData; + // XXX: This is for Scalar support + // TODO: Fix scalar + // eslint-disable-next-line @typescript-eslint/no-explicit-any + visible?: any; } diff --git a/src/interfaces/IWidgetApiResponse.ts b/src/interfaces/IWidgetApiResponse.ts index 2347b6f..07dbc64 100644 --- a/src/interfaces/IWidgetApiResponse.ts +++ b/src/interfaces/IWidgetApiResponse.ts @@ -17,13 +17,14 @@ import { IWidgetApiRequest } from "./IWidgetApiRequest"; export interface IWidgetApiResponseData { - [key: string]: unknown; + [key: string]: unknown; } -export interface IWidgetApiAcknowledgeResponseData extends IWidgetApiResponseData { - // nothing +export interface IWidgetApiAcknowledgeResponseData + extends IWidgetApiResponseData { + // nothing } export interface IWidgetApiResponse extends IWidgetApiRequest { - response: IWidgetApiResponseData; + response: IWidgetApiResponseData; } diff --git a/src/interfaces/LanguageChangeAction.ts b/src/interfaces/LanguageChangeAction.ts index 8b5de3a..feec8fb 100644 --- a/src/interfaces/LanguageChangeAction.ts +++ b/src/interfaces/LanguageChangeAction.ts @@ -18,18 +18,20 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; -export interface ILanguageChangeActionRequestData extends IWidgetApiRequestData { - /** - * The BCP 47 identifier for the client's current language. - */ - lang: string; +export interface ILanguageChangeActionRequestData + extends IWidgetApiRequestData { + /** + * The BCP 47 identifier for the client's current language. + */ + lang: string; } export interface ILanguageChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.LanguageChange; - data: ILanguageChangeActionRequestData; + action: WidgetApiToWidgetAction.LanguageChange; + data: ILanguageChangeActionRequestData; } -export interface ILanguageChangeActionResponse extends ILanguageChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData; +export interface ILanguageChangeActionResponse + extends ILanguageChangeActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/ModalButtonKind.ts b/src/interfaces/ModalButtonKind.ts index e82c939..a6a304c 100644 --- a/src/interfaces/ModalButtonKind.ts +++ b/src/interfaces/ModalButtonKind.ts @@ -15,9 +15,9 @@ */ export enum ModalButtonKind { - Primary = "m.primary", - Secondary = "m.secondary", - Warning = "m.warning", - Danger = "m.danger", - Link = "m.link", + Primary = "m.primary", + Secondary = "m.secondary", + Warning = "m.warning", + Danger = "m.danger", + Link = "m.link", } diff --git a/src/interfaces/ModalWidgetActions.ts b/src/interfaces/ModalWidgetActions.ts index b8f07d4..0073d0c 100644 --- a/src/interfaces/ModalWidgetActions.ts +++ b/src/interfaces/ModalWidgetActions.ts @@ -15,75 +15,86 @@ */ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; +import { + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse"; import { IWidget } from "./IWidget"; import { ModalButtonKind } from "./ModalButtonKind"; export enum BuiltInModalButtonID { - Close = "m.close", + Close = "m.close", } export type ModalButtonID = BuiltInModalButtonID | string; export interface IModalWidgetCreateData extends IWidgetApiRequestData { - [key: string]: unknown; + [key: string]: unknown; } export interface IModalWidgetReturnData { - [key: string]: unknown; + [key: string]: unknown; } // Types for a normal modal requesting the opening a modal widget export interface IModalWidgetOpenRequestDataButton { - id: ModalButtonID; - label: string; - kind: ModalButtonKind | string; - disabled?: boolean; + id: ModalButtonID; + label: string; + kind: ModalButtonKind | string; + disabled?: boolean; } -export interface IModalWidgetOpenRequestData extends IModalWidgetCreateData, Omit { - buttons?: IModalWidgetOpenRequestDataButton[]; +export interface IModalWidgetOpenRequestData + extends IModalWidgetCreateData, + Omit { + buttons?: IModalWidgetOpenRequestDataButton[]; } export interface IModalWidgetOpenRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.OpenModalWidget; - data: IModalWidgetOpenRequestData; + action: WidgetApiFromWidgetAction.OpenModalWidget; + data: IModalWidgetOpenRequestData; } export interface IModalWidgetOpenResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } // Types for a modal widget receiving notifications that its buttons have been pressed -export interface IModalWidgetButtonClickedRequestData extends IWidgetApiRequestData { - id: IModalWidgetOpenRequestDataButton["id"]; +export interface IModalWidgetButtonClickedRequestData + extends IWidgetApiRequestData { + id: IModalWidgetOpenRequestDataButton["id"]; } export interface IModalWidgetButtonClickedRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ButtonClicked; - data: IModalWidgetButtonClickedRequestData; + action: WidgetApiToWidgetAction.ButtonClicked; + data: IModalWidgetButtonClickedRequestData; } export interface IModalWidgetButtonClickedResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } // Types for a modal widget requesting close export interface IModalWidgetCloseRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.CloseModalWidget; - data: IModalWidgetReturnData; + action: WidgetApiFromWidgetAction.CloseModalWidget; + data: IModalWidgetReturnData; } export interface IModalWidgetCloseResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } // Types for a normal widget being notified that the modal widget it opened has been closed -export interface IModalWidgetCloseNotificationRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.CloseModalWidget; - data: IModalWidgetReturnData; +export interface IModalWidgetCloseNotificationRequest + extends IWidgetApiRequest { + action: WidgetApiToWidgetAction.CloseModalWidget; + data: IModalWidgetReturnData; } -export interface IModalWidgetCloseNotificationResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; +export interface IModalWidgetCloseNotificationResponse + extends IWidgetApiResponse { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/NavigateAction.ts b/src/interfaces/NavigateAction.ts index 04960eb..dd6663e 100644 --- a/src/interfaces/NavigateAction.ts +++ b/src/interfaces/NavigateAction.ts @@ -19,14 +19,14 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface INavigateActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2931Navigate; - data: INavigateActionRequestData; + action: WidgetApiFromWidgetAction.MSC2931Navigate; + data: INavigateActionRequestData; } export interface INavigateActionRequestData extends IWidgetApiRequestData { - uri: string; + uri: string; } export interface INavigateActionResponse extends INavigateActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/OpenIDCredentialsAction.ts b/src/interfaces/OpenIDCredentialsAction.ts index c4766f1..d079208 100644 --- a/src/interfaces/OpenIDCredentialsAction.ts +++ b/src/interfaces/OpenIDCredentialsAction.ts @@ -19,20 +19,24 @@ import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { IOpenIDCredentials, OpenIDRequestState } from "./GetOpenIDAction"; -export interface IOpenIDCredentialsActionRequestData extends IWidgetApiRequestData, IOpenIDCredentials { - state: OpenIDRequestState; - original_request_id: string; // eslint-disable-line camelcase +export interface IOpenIDCredentialsActionRequestData + extends IWidgetApiRequestData, + IOpenIDCredentials { + state: OpenIDRequestState; + original_request_id: string; // eslint-disable-line camelcase } export interface IOpenIDCredentialsActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.OpenIDCredentials; - data: IOpenIDCredentialsActionRequestData; + action: WidgetApiToWidgetAction.OpenIDCredentials; + data: IOpenIDCredentialsActionRequestData; } -export interface IOpenIDCredentialsActionResponseData extends IWidgetApiResponseData { - // nothing +export interface IOpenIDCredentialsActionResponseData + extends IWidgetApiResponseData { + // nothing } -export interface IOpenIDCredentialsIDActionResponse extends IOpenIDCredentialsActionRequest { - response: IOpenIDCredentialsActionResponseData; +export interface IOpenIDCredentialsIDActionResponse + extends IOpenIDCredentialsActionRequest { + response: IOpenIDCredentialsActionResponseData; } diff --git a/src/interfaces/ReadEventAction.ts b/src/interfaces/ReadEventAction.ts index 3176989..d1fdd87 100644 --- a/src/interfaces/ReadEventAction.ts +++ b/src/interfaces/ReadEventAction.ts @@ -21,23 +21,25 @@ import { IRoomEvent } from "./IRoomEvent"; import { Symbols } from "../Symbols"; export interface IReadEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string | boolean; // eslint-disable-line camelcase - msgtype?: string; - type: string; - limit?: number; - room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase - since?: string; + state_key?: string | boolean; // eslint-disable-line camelcase + msgtype?: string; + type: string; + limit?: number; + room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase + since?: string; } export interface IReadEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2876ReadEvents; - data: IReadEventFromWidgetRequestData; + action: WidgetApiFromWidgetAction.MSC2876ReadEvents; + data: IReadEventFromWidgetRequestData; } -export interface IReadEventFromWidgetResponseData extends IWidgetApiResponseData { - events: IRoomEvent[]; +export interface IReadEventFromWidgetResponseData + extends IWidgetApiResponseData { + events: IRoomEvent[]; } -export interface IReadEventFromWidgetActionResponse extends IReadEventFromWidgetActionRequest { - response: IReadEventFromWidgetResponseData; +export interface IReadEventFromWidgetActionResponse + extends IReadEventFromWidgetActionRequest { + response: IReadEventFromWidgetResponseData; } diff --git a/src/interfaces/ReadRelationsAction.ts b/src/interfaces/ReadRelationsAction.ts index d89d538..7081756 100644 --- a/src/interfaces/ReadRelationsAction.ts +++ b/src/interfaces/ReadRelationsAction.ts @@ -19,30 +19,34 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IReadRelationsFromWidgetRequestData extends IWidgetApiRequestData { - event_id: string; // eslint-disable-line camelcase - rel_type?: string; // eslint-disable-line camelcase - event_type?: string; // eslint-disable-line camelcase - room_id?: string; // eslint-disable-line camelcase +export interface IReadRelationsFromWidgetRequestData + extends IWidgetApiRequestData { + event_id: string; // eslint-disable-line camelcase + rel_type?: string; // eslint-disable-line camelcase + event_type?: string; // eslint-disable-line camelcase + room_id?: string; // eslint-disable-line camelcase - limit?: number; - from?: string; - to?: string; - direction?: "f" | "b"; + limit?: number; + from?: string; + to?: string; + direction?: "f" | "b"; } -export interface IReadRelationsFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3869ReadRelations; - data: IReadRelationsFromWidgetRequestData; +export interface IReadRelationsFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3869ReadRelations; + data: IReadRelationsFromWidgetRequestData; } -export interface IReadRelationsFromWidgetResponseData extends IWidgetApiResponseData { - chunk: IRoomEvent[]; +export interface IReadRelationsFromWidgetResponseData + extends IWidgetApiResponseData { + chunk: IRoomEvent[]; - next_batch?: string; // eslint-disable-line camelcase - prev_batch?: string; // eslint-disable-line camelcase + next_batch?: string; // eslint-disable-line camelcase + prev_batch?: string; // eslint-disable-line camelcase } -export interface IReadRelationsFromWidgetActionResponse extends IReadRelationsFromWidgetActionRequest { - response: IReadRelationsFromWidgetResponseData; +export interface IReadRelationsFromWidgetActionResponse + extends IReadRelationsFromWidgetActionRequest { + response: IReadRelationsFromWidgetResponseData; } diff --git a/src/interfaces/ReadRoomAccountDataAction.ts b/src/interfaces/ReadRoomAccountDataAction.ts index 15c1201..43c5204 100644 --- a/src/interfaces/ReadRoomAccountDataAction.ts +++ b/src/interfaces/ReadRoomAccountDataAction.ts @@ -20,20 +20,24 @@ import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { IRoomAccountData } from "./IRoomAccountData"; import { Symbols } from "../Symbols"; -export interface IReadRoomAccountDataFromWidgetRequestData extends IWidgetApiRequestData { - type: string; - room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase +export interface IReadRoomAccountDataFromWidgetRequestData + extends IWidgetApiRequestData { + type: string; + room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase } -export interface IReadRoomAccountDataFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData; - data: IReadRoomAccountDataFromWidgetRequestData; +export interface IReadRoomAccountDataFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData; + data: IReadRoomAccountDataFromWidgetRequestData; } -export interface IReadRoomAccountDataFromWidgetResponseData extends IWidgetApiResponseData { - events: IRoomAccountData[]; +export interface IReadRoomAccountDataFromWidgetResponseData + extends IWidgetApiResponseData { + events: IRoomAccountData[]; } -export interface IReadRoomAccountDataFromWidgetActionResponse extends IReadRoomAccountDataFromWidgetActionRequest { - response: IReadRoomAccountDataFromWidgetResponseData; +export interface IReadRoomAccountDataFromWidgetActionResponse + extends IReadRoomAccountDataFromWidgetActionRequest { + response: IReadRoomAccountDataFromWidgetResponseData; } diff --git a/src/interfaces/ScreenshotAction.ts b/src/interfaces/ScreenshotAction.ts index f9ec315..bfa3008 100644 --- a/src/interfaces/ScreenshotAction.ts +++ b/src/interfaces/ScreenshotAction.ts @@ -14,19 +14,22 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest"; import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface IScreenshotActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.TakeScreenshot; - data: IWidgetApiRequestEmptyData; + action: WidgetApiToWidgetAction.TakeScreenshot; + data: IWidgetApiRequestEmptyData; } export interface IScreenshotActionResponseData extends IWidgetApiResponseData { - screenshot: Blob; + screenshot: Blob; } export interface IScreenshotActionResponse extends IScreenshotActionRequest { - response: IScreenshotActionResponseData; + response: IScreenshotActionResponseData; } diff --git a/src/interfaces/SendEventAction.ts b/src/interfaces/SendEventAction.ts index 4631dac..ba89b58 100644 --- a/src/interfaces/SendEventAction.ts +++ b/src/interfaces/SendEventAction.ts @@ -15,49 +15,57 @@ */ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { IRoomEvent } from "./IRoomEvent"; export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string; // eslint-disable-line camelcase - type: string; - content: unknown; - room_id?: string; // eslint-disable-line camelcase - - // MSC4157 - delay?: number; // eslint-disable-line camelcase - parent_delay_id?: string; // eslint-disable-line camelcase + state_key?: string; // eslint-disable-line camelcase + type: string; + content: unknown; + room_id?: string; // eslint-disable-line camelcase + + // MSC4157 + delay?: number; // eslint-disable-line camelcase + parent_delay_id?: string; // eslint-disable-line camelcase } export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendEvent; - data: ISendEventFromWidgetRequestData; + action: WidgetApiFromWidgetAction.SendEvent; + data: ISendEventFromWidgetRequestData; } -export interface ISendEventFromWidgetResponseData extends IWidgetApiResponseData { - room_id: string; // eslint-disable-line camelcase - event_id?: string; // eslint-disable-line camelcase +export interface ISendEventFromWidgetResponseData + extends IWidgetApiResponseData { + room_id: string; // eslint-disable-line camelcase + event_id?: string; // eslint-disable-line camelcase - // MSC4157 - delay_id?: string; // eslint-disable-line camelcase + // MSC4157 + delay_id?: string; // eslint-disable-line camelcase } -export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidgetActionRequest { - response: ISendEventFromWidgetResponseData; +export interface ISendEventFromWidgetActionResponse + extends ISendEventFromWidgetActionRequest { + response: ISendEventFromWidgetResponseData; } -export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {} +export interface ISendEventToWidgetRequestData + extends IWidgetApiRequestData, + IRoomEvent {} export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendEvent; - data: ISendEventToWidgetRequestData; + action: WidgetApiToWidgetAction.SendEvent; + data: ISendEventToWidgetRequestData; } export interface ISendEventToWidgetResponseData extends IWidgetApiResponseData { - // nothing + // nothing } -export interface ISendEventToWidgetActionResponse extends ISendEventToWidgetActionRequest { - response: ISendEventToWidgetResponseData; +export interface ISendEventToWidgetActionResponse + extends ISendEventToWidgetActionRequest { + response: ISendEventToWidgetResponseData; } diff --git a/src/interfaces/SendToDeviceAction.ts b/src/interfaces/SendToDeviceAction.ts index e7507b3..920119f 100644 --- a/src/interfaces/SendToDeviceAction.ts +++ b/src/interfaces/SendToDeviceAction.ts @@ -15,42 +15,53 @@ */ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { IRoomEvent } from "./IRoomEvent"; -export interface ISendToDeviceFromWidgetRequestData extends IWidgetApiRequestData { - type: string; - encrypted: boolean; - messages: { [userId: string]: { [deviceId: string]: object } }; +export interface ISendToDeviceFromWidgetRequestData + extends IWidgetApiRequestData { + type: string; + encrypted: boolean; + messages: { [userId: string]: { [deviceId: string]: object } }; } -export interface ISendToDeviceFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendToDevice; - data: ISendToDeviceFromWidgetRequestData; +export interface ISendToDeviceFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.SendToDevice; + data: ISendToDeviceFromWidgetRequestData; } -export interface ISendToDeviceFromWidgetResponseData extends IWidgetApiResponseData { - // nothing +export interface ISendToDeviceFromWidgetResponseData + extends IWidgetApiResponseData { + // nothing } -export interface ISendToDeviceFromWidgetActionResponse extends ISendToDeviceFromWidgetActionRequest { - response: ISendToDeviceFromWidgetResponseData; +export interface ISendToDeviceFromWidgetActionResponse + extends ISendToDeviceFromWidgetActionRequest { + response: ISendToDeviceFromWidgetResponseData; } -export interface ISendToDeviceToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent { - encrypted: boolean; +export interface ISendToDeviceToWidgetRequestData + extends IWidgetApiRequestData, + IRoomEvent { + encrypted: boolean; } export interface ISendToDeviceToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendToDevice; - data: ISendToDeviceToWidgetRequestData; + action: WidgetApiToWidgetAction.SendToDevice; + data: ISendToDeviceToWidgetRequestData; } -export interface ISendToDeviceToWidgetResponseData extends IWidgetApiResponseData { - // nothing +export interface ISendToDeviceToWidgetResponseData + extends IWidgetApiResponseData { + // nothing } -export interface ISendToDeviceToWidgetActionResponse extends ISendToDeviceToWidgetActionRequest { - response: ISendToDeviceToWidgetResponseData; +export interface ISendToDeviceToWidgetActionResponse + extends ISendToDeviceToWidgetActionRequest { + response: ISendToDeviceToWidgetResponseData; } From ac15148fecf584b2a21ed739db4129dd0d30fb0d Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:49:01 +0100 Subject: [PATCH 07/11] run prettier --- src/Symbols.ts | 2 +- src/WidgetApi.ts | 1757 +++--- src/interfaces/SetModalButtonEnabledAction.ts | 16 +- src/interfaces/StickerAction.ts | 40 +- src/interfaces/StickyAction.ts | 10 +- src/interfaces/SupportedVersionsAction.ts | 26 +- src/interfaces/ThemeChangeAction.ts | 8 +- src/interfaces/TurnServerActions.ts | 44 +- src/interfaces/UpdateDelayedEventAction.ts | 30 +- src/interfaces/UpdateStateAction.ts | 16 +- src/interfaces/UploadFileAction.ts | 22 +- src/interfaces/UserDirectorySearchAction.ts | 34 +- src/interfaces/VisibilityAction.ts | 8 +- src/interfaces/WidgetApiAction.ts | 133 +- src/interfaces/WidgetApiDirection.ts | 18 +- src/interfaces/WidgetConfigAction.ts | 11 +- src/interfaces/WidgetKind.ts | 6 +- src/interfaces/WidgetType.ts | 6 +- src/models/Widget.ts | 146 +- src/models/WidgetEventCapability.ts | 437 +- src/models/WidgetParser.ts | 233 +- src/models/validation/url.ts | 24 +- src/models/validation/utils.ts | 11 +- src/templating/url-template.ts | 88 +- src/transport/ITransport.ts | 150 +- src/transport/PostmessageTransport.ts | 335 +- src/util/SimpleObservable.ts | 28 +- test/ClientWidgetApi-test.ts | 4708 +++++++++-------- test/WidgetApi-test.ts | 1510 +++--- test/url-template-test.ts | 74 +- 30 files changed, 5261 insertions(+), 4670 deletions(-) diff --git a/src/Symbols.ts b/src/Symbols.ts index 85ca12e..04ee9d0 100644 --- a/src/Symbols.ts +++ b/src/Symbols.ts @@ -15,5 +15,5 @@ */ export enum Symbols { - AnyRoom = "*", + AnyRoom = "*", } diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 44f0de9..4793bc6 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -17,96 +17,124 @@ import { EventEmitter } from "events"; import { Capability } from "./interfaces/Capabilities"; -import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest"; +import { + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./interfaces/IWidgetApiRequest"; import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse"; import { WidgetApiDirection } from "./interfaces/WidgetApiDirection"; import { - ISupportedVersionsActionRequest, - ISupportedVersionsActionResponseData, + ISupportedVersionsActionRequest, + ISupportedVersionsActionResponseData, } from "./interfaces/SupportedVersionsAction"; -import { ApiVersion, CurrentApiVersions, UnstableApiVersion } from "./interfaces/ApiVersion"; import { - ICapabilitiesActionRequest, - ICapabilitiesActionResponseData, - INotifyCapabilitiesActionRequest, - IRenegotiateCapabilitiesRequestData, + 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, IWidgetApiErrorResponseDataDetails } from "./interfaces/IWidgetApiErrorResponse"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./interfaces/WidgetApiAction"; +import { + IWidgetApiErrorResponseData, + IWidgetApiErrorResponseDataDetails, +} from "./interfaces/IWidgetApiErrorResponse"; import { IStickerActionRequestData } from "./interfaces/StickerAction"; -import { IStickyActionRequestData, IStickyActionResponseData } from "./interfaces/StickyAction"; import { - IGetOpenIDActionRequestData, - IGetOpenIDActionResponse, - IOpenIDCredentials, - OpenIDRequestState, + 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, + BuiltInModalButtonID, + IModalWidgetCreateData, + IModalWidgetOpenRequestData, + IModalWidgetOpenRequestDataButton, + IModalWidgetReturnData, + ModalButtonID, } from "./interfaces/ModalWidgetActions"; import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalButtonEnabledAction"; -import { ISendEventFromWidgetRequestData, ISendEventFromWidgetResponseData } from "./interfaces/SendEventAction"; import { - ISendToDeviceFromWidgetRequestData, - ISendToDeviceFromWidgetResponseData, + ISendEventFromWidgetRequestData, + ISendEventFromWidgetResponseData, +} from "./interfaces/SendEventAction"; +import { + ISendToDeviceFromWidgetRequestData, + ISendToDeviceFromWidgetResponseData, } from "./interfaces/SendToDeviceAction"; -import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability"; +import { + EventDirection, + WidgetEventCapability, +} from "./models/WidgetEventCapability"; import { INavigateActionRequestData } from "./interfaces/NavigateAction"; -import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction"; import { - IReadRoomAccountDataFromWidgetRequestData, - IReadRoomAccountDataFromWidgetResponseData, + IReadEventFromWidgetRequestData, + IReadEventFromWidgetResponseData, +} from "./interfaces/ReadEventAction"; +import { + IReadRoomAccountDataFromWidgetRequestData, + IReadRoomAccountDataFromWidgetResponseData, } from "./interfaces/ReadRoomAccountDataAction"; import { IRoomEvent } from "./interfaces/IRoomEvent"; import { IRoomAccountData } from "./interfaces/IRoomAccountData"; -import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions"; +import { + ITurnServer, + IUpdateTurnServersRequest, +} from "./interfaces/TurnServerActions"; import { Symbols } from "./Symbols"; import { - IReadRelationsFromWidgetRequestData, - IReadRelationsFromWidgetResponseData, + IReadRelationsFromWidgetRequestData, + IReadRelationsFromWidgetResponseData, } from "./interfaces/ReadRelationsAction"; import { - IUserDirectorySearchFromWidgetRequestData, - IUserDirectorySearchFromWidgetResponseData, + IUserDirectorySearchFromWidgetRequestData, + IUserDirectorySearchFromWidgetResponseData, } from "./interfaces/UserDirectorySearchAction"; import { - IGetMediaConfigActionFromWidgetRequestData, - IGetMediaConfigActionFromWidgetResponseData, + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData, } from "./interfaces/GetMediaConfigAction"; import { - IUploadFileActionFromWidgetRequestData, - IUploadFileActionFromWidgetResponseData, + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData, } from "./interfaces/UploadFileAction"; import { - IDownloadFileActionFromWidgetRequestData, - IDownloadFileActionFromWidgetResponseData, + IDownloadFileActionFromWidgetRequestData, + IDownloadFileActionFromWidgetResponseData, } from "./interfaces/DownloadFileAction"; import { - IUpdateDelayedEventFromWidgetRequestData, - IUpdateDelayedEventFromWidgetResponseData, - UpdateDelayedEventAction, + IUpdateDelayedEventFromWidgetRequestData, + IUpdateDelayedEventFromWidgetResponseData, + UpdateDelayedEventAction, } from "./interfaces/UpdateDelayedEventAction"; export class WidgetApiResponseError extends Error { - static { - this.prototype.name = this.name; - } - - public constructor( - message: string, - public readonly data: IWidgetApiErrorResponseDataDetails, - ) { - super(message); - } + static { + this.prototype.name = this.name; + } + + public constructor( + message: string, + public readonly data: IWidgetApiErrorResponseDataDetails, + ) { + super(message); + } } /** @@ -127,777 +155,930 @@ export class WidgetApiResponseError extends Error { * 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): void { - 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[]): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Send, msgtype).raw); + 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.", + ); } - - /** - * 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): void { - this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype).raw); + 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); } - - /** - * Requests the capability to receive a given item in room account data. 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. - */ - public requestCapabilityToReceiveRoomAccountData(eventType: string): void { - this.requestCapability(WidgetEventCapability.forRoomAccountData(EventDirection.Receive, eventType).raw); + 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): void { + if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) { + throw new Error("Capabilities have already been negotiated"); } - /** - * 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} Resolves to a token for verification. - * @throws Throws if the user rejected the request or the request failed. - */ - public requestOpenIDConnectToken(): Promise { - return new Promise((resolve, reject) => { - this.transport - .sendComplete( - 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): void => { - 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, {}); // ack - } else if (request.data.state === OpenIDRequestState.Blocked) { - reject(new Error("User declined to verify their identity")); - this.transport.reply(request, {}); // ack - } else { - reject(new Error("Invalid state on reply: " + rdata.state)); - this.transport.reply(request, { - 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); - }); + 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[]): void { + 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, + ): void { + 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, + ): void { + 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, + ): void { + 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): void { + 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): void { + 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): void { + 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): void { + 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): void { + 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): void { + this.requestCapability( + WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype) + .raw, + ); + } + + /** + * Requests the capability to receive a given item in room account data. 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. + */ + public requestCapabilityToReceiveRoomAccountData(eventType: string): void { + this.requestCapability( + WidgetEventCapability.forRoomAccountData( + EventDirection.Receive, + eventType, + ).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} Resolves to a token for verification. + * @throws Throws if the user rejected the request or the request failed. + */ + public requestOpenIDConnectToken(): Promise { + return new Promise((resolve, reject) => { + this.transport + .sendComplete( + 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, + ): void => { + 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, {}); // ack + } else if (request.data.state === OpenIDRequestState.Blocked) { + reject(new Error("User declined to verify their identity")); + this.transport.reply(request, {}); // ack + } else { + reject(new Error("Invalid state on reply: " + rdata.state)); + this.transport.reply(request, { + 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} 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 { + 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 { + return this.transport + .send( + WidgetApiFromWidgetAction.ContentLoaded, + {}, + ) + .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 { + 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} 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 { + 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} Resolves when the modal widget has been opened. + */ + public openModalWidget( + url: string, + name: string, + buttons: IModalWidgetOpenRequestDataButton[] = [], + data: IModalWidgetCreateData = {}, + type: WidgetType = MatrixWidgetType.Custom, + ): Promise { + return this.transport + .send( + 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} Resolves when complete. + */ + public closeModalWidget(data: IModalWidgetReturnData = {}): Promise { + return this.transport + .send( + WidgetApiFromWidgetAction.CloseModalWidget, + data, + ) + .then(); + } + + public sendRoomEvent( + eventType: string, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.sendEvent( + eventType, + undefined, + content, + roomId, + delay, + parentDelayId, + ); + } + + public sendStateEvent( + eventType: string, + stateKey: string, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.sendEvent( + eventType, + stateKey, + content, + roomId, + delay, + parentDelayId, + ); + } + + private sendEvent( + eventType: string, + stateKey: string | undefined, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.transport.send< + ISendEventFromWidgetRequestData, + ISendEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.SendEvent, { + type: eventType, + content, + ...(stateKey !== undefined && { state_key: stateKey }), + ...(roomId !== undefined && { room_id: roomId }), + ...(delay !== undefined && { delay }), + ...(parentDelayId !== undefined && { parent_delay_id: parentDelayId }), + }); + } + + /** + * @deprecated This currently relies on an unstable MSC (MSC4157). + */ + public updateDelayedEvent( + delayId: string, + action: UpdateDelayedEventAction, + ): Promise { + return this.transport.send< + IUpdateDelayedEventFromWidgetRequestData, + IUpdateDelayedEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, { + delay_id: delayId, + action, + }); + } + + /** + * 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} Resolves when complete. + */ + public sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + return this.transport.send< + ISendToDeviceFromWidgetRequestData, + ISendToDeviceFromWidgetResponseData + >(WidgetApiFromWidgetAction.SendToDevice, { + type: eventType, + encrypted, + messages: contentMap, + }); + } + + public readRoomAccountData( + eventType: string, + roomIds?: (string | Symbols.AnyRoom)[], + ): Promise { + const data: IReadEventFromWidgetRequestData = { type: eventType }; + + if (roomIds) { + if (roomIds.includes(Symbols.AnyRoom)) { + data.room_ids = Symbols.AnyRoom; + } else { + data.room_ids = roomIds; + } } - - /** - * Asks the client for additional capabilities. Capabilities can be queued for this - * request with the requestCapability() functions. - * @returns {Promise} 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 { - return this.transport - .send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, { - capabilities: this.requestedCapabilities, - }) - .then(); + return this.transport + .send< + IReadRoomAccountDataFromWidgetRequestData, + IReadRoomAccountDataFromWidgetResponseData + >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) + .then((r) => r.events); + } + + public readRoomEvents( + eventType: string, + limit?: number, + msgtype?: string, + roomIds?: (string | Symbols.AnyRoom)[], + since?: string | undefined, + ): Promise { + const data: IReadEventFromWidgetRequestData = { + type: eventType, + msgtype: msgtype, + }; + if (limit !== undefined) { + data.limit = limit; } - - /** - * Tell the client that the content has been loaded. - * @returns {Promise} Resolves when the client acknowledges the request. - */ - public sendContentLoaded(): Promise { - return this.transport.send(WidgetApiFromWidgetAction.ContentLoaded, {}).then(); + if (roomIds) { + if (roomIds.includes(Symbols.AnyRoom)) { + data.room_ids = Symbols.AnyRoom; + } else { + data.room_ids = roomIds; + } } - - /** - * 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 { - return this.transport.send(WidgetApiFromWidgetAction.SendSticker, sticker).then(); + if (since) { + data.since = since; } - - /** - * Asks the client to set the always-on-screen status for this widget. - * @param {boolean} value The new state to request. - * @returns {Promise} 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 { - return this.transport - .send< - IStickyActionRequestData, - IStickyActionResponseData - >(WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, { value }) - .then((res) => res.success); + 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 { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC3869)) { + throw new Error( + "The read_relations action is not supported by the client.", + ); } - /** - * 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} Resolves when the modal widget has been opened. - */ - public openModalWidget( - url: string, - name: string, - buttons: IModalWidgetOpenRequestDataButton[] = [], - data: IModalWidgetCreateData = {}, - type: WidgetType = MatrixWidgetType.Custom, - ): Promise { - return this.transport - .send(WidgetApiFromWidgetAction.OpenModalWidget, { - type, - url, - name, - buttons, - data, - }) - .then(); + 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 { + const data: IReadEventFromWidgetRequestData = { + type: eventType, + state_key: stateKey === undefined ? true : stateKey, + }; + if (limit !== undefined) { + data.limit = limit; } - - /** - * 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} Resolves when complete. - */ - public closeModalWidget(data: IModalWidgetReturnData = {}): Promise { - return this.transport.send(WidgetApiFromWidgetAction.CloseModalWidget, data).then(); + if (roomIds) { + if (roomIds.includes(Symbols.AnyRoom)) { + data.room_ids = Symbols.AnyRoom; + } else { + data.room_ids = roomIds; + } } - - public sendRoomEvent( - eventType: string, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.sendEvent(eventType, undefined, content, roomId, delay, parentDelayId); + 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} 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 { + if (buttonId === BuiltInModalButtonID.Close) { + throw new Error("The close button cannot be disabled"); } - - public sendStateEvent( - eventType: string, - stateKey: string, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.sendEvent(eventType, stateKey, content, roomId, delay, parentDelayId); + return this.transport + .send( + 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} 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 { + if (!uri || !uri.startsWith("https://matrix.to/#")) { + throw new Error("Invalid matrix.to URI"); } - private sendEvent( - eventType: string, - stateKey: string | undefined, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.transport.send( - WidgetApiFromWidgetAction.SendEvent, - { - type: eventType, - content, - ...(stateKey !== undefined && { state_key: stateKey }), - ...(roomId !== undefined && { room_id: roomId }), - ...(delay !== undefined && { delay }), - ...(parentDelayId !== undefined && { parent_delay_id: parentDelayId }), - }, + return this.transport + .send( + 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 { + let setTurnServer: (server: ITurnServer) => void; + + const onUpdateTurnServers = async ( + ev: CustomEvent, + ): Promise => { + ev.preventDefault(); + setTurnServer(ev.detail.data); + await this.transport.reply( + 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( + WidgetApiFromWidgetAction.WatchTurnServers, + {}, ); - } - - /** - * @deprecated This currently relies on an unstable MSC (MSC4157). - */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { - return this.transport.send( - WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - { - delay_id: delayId, - action, - }, + } catch (e) { + this.off( + `action:${WidgetApiToWidgetAction.UpdateTurnServers}`, + onUpdateTurnServers, ); + throw e; + } } + this.turnServerWatchers++; - /** - * 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} Resolves when complete. - */ - public sendToDevice( - eventType: string, - encrypted: boolean, - contentMap: { [userId: string]: { [deviceId: string]: object } }, - ): Promise { - return this.transport.send( - WidgetApiFromWidgetAction.SendToDevice, - { type: eventType, encrypted, messages: contentMap }, + try { + // Watch for new data indefinitely (until this generator's return method is called) + while (true) { + yield await new Promise( + (resolve) => (setTurnServer = resolve), ); - } - - public readRoomAccountData(eventType: string, roomIds?: (string | Symbols.AnyRoom)[]): Promise { - const data: IReadEventFromWidgetRequestData = { type: eventType }; - - if (roomIds) { - if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom; - } else { - data.room_ids = roomIds; - } - } - return this.transport - .send< - IReadRoomAccountDataFromWidgetRequestData, - IReadRoomAccountDataFromWidgetResponseData - >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) - .then((r) => r.events); - } - - public readRoomEvents( - eventType: string, - limit?: number, - msgtype?: string, - roomIds?: (string | Symbols.AnyRoom)[], - since?: string | undefined, - ): Promise { - 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; - } - } - if (since) { - data.since = since; - } - 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 { - 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( - WidgetApiFromWidgetAction.MSC3869ReadRelations, - data, + } + } 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( + WidgetApiFromWidgetAction.UnwatchTurnServers, + {}, ); + } } - - public readStateEvents( - eventType: string, - limit?: number, - stateKey?: string, - roomIds?: (string | Symbols.AnyRoom)[], - ): Promise { - 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} 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 { - if (buttonId === BuiltInModalButtonID.Close) { - throw new Error("The close button cannot be disabled"); - } - return this.transport - .send(WidgetApiFromWidgetAction.SetModalButtonEnabled, { - button: buttonId, - enabled: isEnabled, - }) - .then(); + } + + /** + * 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 { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC3973)) { + throw new Error( + "The user_directory_search action is not supported by the client.", + ); } - /** - * 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} 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 { - if (!uri || !uri.startsWith("https://matrix.to/#")) { - throw new Error("Invalid matrix.to URI"); - } - - return this.transport - .send(WidgetApiFromWidgetAction.MSC2931Navigate, { uri }) - .then(); + const data: IUserDirectorySearchFromWidgetRequestData = { + search_term: searchTerm, + limit, + }; + + return this.transport.send< + IUserDirectorySearchFromWidgetRequestData, + IUserDirectorySearchFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data); + } + + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public async getMediaConfig(): Promise { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error( + "The get_media_config action is not supported by the client.", + ); } - /** - * 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 { - let setTurnServer: (server: ITurnServer) => void; - - const onUpdateTurnServers = async (ev: CustomEvent): Promise => { - ev.preventDefault(); - setTurnServer(ev.detail.data); - await this.transport.reply(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(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((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(WidgetApiFromWidgetAction.UnwatchTurnServers, {}); - } - } + const data: IGetMediaConfigActionFromWidgetRequestData = {}; + + return this.transport.send< + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data); + } + + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public async uploadFile( + file: XMLHttpRequestBodyInit, + ): Promise { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error("The upload_file action is not supported by the client."); } - /** - * 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 { - 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); + const data: IUploadFileActionFromWidgetRequestData = { + file, + }; + + return this.transport.send< + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data); + } + + /** + * Download a file from the media repository on the homeserver. + * @param contentUri - MXC URI of the file to download. + * @returns Resolves to the contents of the file. + */ + public async downloadFile( + contentUri: string, + ): Promise { + const versions = await this.getClientVersions(); + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error( + "The download_file action is not supported by the client.", + ); } - /** - * Get the config for the media repository. - * @returns Promise which resolves with an object containing the config. - */ - public async getMediaConfig(): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error("The get_media_config action is not supported by the client."); - } - - const data: IGetMediaConfigActionFromWidgetRequestData = {}; - - return this.transport.send< - IGetMediaConfigActionFromWidgetRequestData, - IGetMediaConfigActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data); - } - - /** - * Upload a file to the media repository on the homeserver. - * @param file - The object to upload. Something that can be sent to - * XMLHttpRequest.send (typically a File). - * @returns Resolves to the location of the uploaded file. - */ - public async uploadFile(file: XMLHttpRequestBodyInit): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error("The upload_file action is not supported by the client."); - } - - const data: IUploadFileActionFromWidgetRequestData = { - file, - }; - - return this.transport.send( - WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data, - ); - } - - /** - * Download a file from the media repository on the homeserver. - * @param contentUri - MXC URI of the file to download. - * @returns Resolves to the contents of the file. - */ - public async downloadFile(contentUri: string): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error("The download_file action is not supported by the client."); - } - - const data: IDownloadFileActionFromWidgetRequestData = { - content_uri: contentUri, - }; - - return this.transport.send( - WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - 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(): void { - this.transport.start(); - this.getClientVersions().then((v) => { - if (v.includes(UnstableApiVersion.MSC2974)) { - this.supportsMSC2974Renegotiate = true; - } - }); + const data: IDownloadFileActionFromWidgetRequestData = { + content_uri: contentUri, + }; + + return this.transport.send< + IDownloadFileActionFromWidgetRequestData, + IDownloadFileActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039DownloadFileAction, 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(): void { + this.transport.start(); + this.getClientVersions().then((v) => { + if (v.includes(UnstableApiVersion.MSC2974)) { + this.supportsMSC2974Renegotiate = true; + } + }); + } + + private handleMessage( + ev: CustomEvent, + ): void | Promise { + 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(ev.detail); + case WidgetApiToWidgetAction.Capabilities: + return this.handleCapabilities(ev.detail); + case WidgetApiToWidgetAction.UpdateVisibility: + return this.transport.reply( + ev.detail, + {}, + ); // ack to avoid error spam + case WidgetApiToWidgetAction.NotifyCapabilities: + return this.transport.reply( + ev.detail, + {}, + ); // ack to avoid error spam + default: + return this.transport.reply(ev.detail, { + error: { + message: "Unknown or unsupported action: " + ev.detail.action, + }, + }); + } } + } - private handleMessage(ev: CustomEvent): void | Promise { - 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(ev.detail); - case WidgetApiToWidgetAction.Capabilities: - return this.handleCapabilities(ev.detail); - case WidgetApiToWidgetAction.UpdateVisibility: - return this.transport.reply(ev.detail, {}); // ack to avoid error spam - case WidgetApiToWidgetAction.NotifyCapabilities: - return this.transport.reply(ev.detail, {}); // ack to avoid error spam - default: - return this.transport.reply(ev.detail, { - error: { - message: "Unknown or unsupported action: " + ev.detail.action, - }, - }); - } - } - } + private replyVersions(request: ISupportedVersionsActionRequest): void { + this.transport.reply(request, { + supported_versions: CurrentApiVersions, + }); + } - private replyVersions(request: ISupportedVersionsActionRequest): void { - this.transport.reply(request, { - supported_versions: CurrentApiVersions, - }); + public getClientVersions(): Promise { + if (Array.isArray(this.cachedClientVersions)) { + return Promise.resolve(this.cachedClientVersions); } - public getClientVersions(): Promise { - if (Array.isArray(this.cachedClientVersions)) { - return Promise.resolve(this.cachedClientVersions); - } - - return this.transport - .send( - 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 []; - }); + return this.transport + .send( + 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, + ): void | Promise { + if (this.capabilitiesFinished) { + return this.transport.reply(request, { + error: { + message: "Capability negotiation already completed", + }, + }); } - private handleCapabilities(request: ICapabilitiesActionRequest): void | Promise { - if (this.capabilitiesFinished) { - return this.transport.reply(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) => { - 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(request, { - capabilities: this.requestedCapabilities, - }); - }); - } + // 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) => { + 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(request, { + capabilities: this.requestedCapabilities, + }); + }); + } } diff --git a/src/interfaces/SetModalButtonEnabledAction.ts b/src/interfaces/SetModalButtonEnabledAction.ts index 5702e8c..ada05c6 100644 --- a/src/interfaces/SetModalButtonEnabledAction.ts +++ b/src/interfaces/SetModalButtonEnabledAction.ts @@ -19,16 +19,18 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; import { ModalButtonID } from "./ModalWidgetActions"; -export interface ISetModalButtonEnabledActionRequestData extends IWidgetApiRequestData { - enabled: boolean; - button: ModalButtonID; +export interface ISetModalButtonEnabledActionRequestData + extends IWidgetApiRequestData { + enabled: boolean; + button: ModalButtonID; } export interface ISetModalButtonEnabledActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SetModalButtonEnabled; - data: ISetModalButtonEnabledActionRequestData; + action: WidgetApiFromWidgetAction.SetModalButtonEnabled; + data: ISetModalButtonEnabledActionRequestData; } -export interface ISetModalButtonEnabledActionResponse extends ISetModalButtonEnabledActionRequest { - response: IWidgetApiAcknowledgeResponseData; +export interface ISetModalButtonEnabledActionResponse + extends ISetModalButtonEnabledActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/StickerAction.ts b/src/interfaces/StickerAction.ts index c7293e3..cd401c2 100644 --- a/src/interfaces/StickerAction.ts +++ b/src/interfaces/StickerAction.ts @@ -19,31 +19,31 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IStickerActionRequestData extends IWidgetApiRequestData { - name: string; - description?: string; - content: { - url: string; - info?: { - h?: number; - w?: number; - mimetype?: string; - size?: number; - thumbnail_info?: { - // eslint-disable-line camelcase - h?: number; - w?: number; - mimetype?: string; - size?: number; - }; - }; + name: string; + description?: string; + content: { + url: string; + info?: { + h?: number; + w?: number; + mimetype?: string; + size?: number; + thumbnail_info?: { + // eslint-disable-line camelcase + h?: number; + w?: number; + mimetype?: string; + size?: number; + }; }; + }; } export interface IStickerActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendSticker; - data: IStickerActionRequestData; + action: WidgetApiFromWidgetAction.SendSticker; + data: IStickerActionRequestData; } export interface IStickerActionResponse extends IStickerActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/StickyAction.ts b/src/interfaces/StickyAction.ts index 7d49f02..a9726b8 100644 --- a/src/interfaces/StickyAction.ts +++ b/src/interfaces/StickyAction.ts @@ -19,18 +19,18 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface IStickyActionRequestData extends IWidgetApiRequestData { - value: boolean; + value: boolean; } export interface IStickyActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen; - data: IStickyActionRequestData; + action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen; + data: IStickyActionRequestData; } export interface IStickyActionResponseData extends IWidgetApiResponseData { - success: boolean; + success: boolean; } export interface IStickyActionResponse extends IStickyActionRequest { - response: IStickyActionResponseData; + response: IStickyActionResponseData; } diff --git a/src/interfaces/SupportedVersionsAction.ts b/src/interfaces/SupportedVersionsAction.ts index 8486ebc..ea630e1 100644 --- a/src/interfaces/SupportedVersionsAction.ts +++ b/src/interfaces/SupportedVersionsAction.ts @@ -14,20 +14,30 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; import { ApiVersion } from "./ApiVersion"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface ISupportedVersionsActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SupportedApiVersions | WidgetApiToWidgetAction.SupportedApiVersions; - data: IWidgetApiRequestEmptyData; + action: + | WidgetApiFromWidgetAction.SupportedApiVersions + | WidgetApiToWidgetAction.SupportedApiVersions; + data: IWidgetApiRequestEmptyData; } -export interface ISupportedVersionsActionResponseData extends IWidgetApiResponseData { - supported_versions: ApiVersion[]; // eslint-disable-line camelcase +export interface ISupportedVersionsActionResponseData + extends IWidgetApiResponseData { + supported_versions: ApiVersion[]; // eslint-disable-line camelcase } -export interface ISupportedVersionsActionResponse extends ISupportedVersionsActionRequest { - response: ISupportedVersionsActionResponseData; +export interface ISupportedVersionsActionResponse + extends ISupportedVersionsActionRequest { + response: ISupportedVersionsActionResponseData; } diff --git a/src/interfaces/ThemeChangeAction.ts b/src/interfaces/ThemeChangeAction.ts index 292f58e..9766e20 100644 --- a/src/interfaces/ThemeChangeAction.ts +++ b/src/interfaces/ThemeChangeAction.ts @@ -19,14 +19,14 @@ import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IThemeChangeActionRequestData extends IWidgetApiRequestData { - // The format of a theme is deliberately unstandardized + // The format of a theme is deliberately unstandardized } export interface IThemeChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ThemeChange; - data: IThemeChangeActionRequestData; + action: WidgetApiToWidgetAction.ThemeChange; + data: IThemeChangeActionRequestData; } export interface IThemeChangeActionResponse extends IThemeChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/TurnServerActions.ts b/src/interfaces/TurnServerActions.ts index 36f664a..2bed7f1 100644 --- a/src/interfaces/TurnServerActions.ts +++ b/src/interfaces/TurnServerActions.ts @@ -14,41 +14,53 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; +import { + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction"; +import { + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse"; export interface ITurnServer { - uris: string[]; - username: string; - password: string; + uris: string[]; + username: string; + password: string; } export interface IWatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.WatchTurnServers; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.WatchTurnServers; + data: IWidgetApiRequestEmptyData; } export interface IWatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } export interface IUnwatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UnwatchTurnServers; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.UnwatchTurnServers; + data: IWidgetApiRequestEmptyData; } export interface IUnwatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } -export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer {} +export interface IUpdateTurnServersRequestData + extends IWidgetApiRequestData, + ITurnServer {} export interface IUpdateTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateTurnServers; - data: IUpdateTurnServersRequestData; + action: WidgetApiToWidgetAction.UpdateTurnServers; + data: IUpdateTurnServersRequestData; } export interface IUpdateTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/UpdateDelayedEventAction.ts b/src/interfaces/UpdateDelayedEventAction.ts index 9ba0179..92ba659 100644 --- a/src/interfaces/UpdateDelayedEventAction.ts +++ b/src/interfaces/UpdateDelayedEventAction.ts @@ -19,25 +19,29 @@ import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export enum UpdateDelayedEventAction { - Cancel = "cancel", - Restart = "restart", - Send = "send", + Cancel = "cancel", + Restart = "restart", + Send = "send", } -export interface IUpdateDelayedEventFromWidgetRequestData extends IWidgetApiRequestData { - delay_id: string; // eslint-disable-line camelcase - action: UpdateDelayedEventAction; +export interface IUpdateDelayedEventFromWidgetRequestData + extends IWidgetApiRequestData { + delay_id: string; // eslint-disable-line camelcase + action: UpdateDelayedEventAction; } -export interface IUpdateDelayedEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent; - data: IUpdateDelayedEventFromWidgetRequestData; +export interface IUpdateDelayedEventFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent; + data: IUpdateDelayedEventFromWidgetRequestData; } -export interface IUpdateDelayedEventFromWidgetResponseData extends IWidgetApiResponseData { - // nothing +export interface IUpdateDelayedEventFromWidgetResponseData + extends IWidgetApiResponseData { + // nothing } -export interface IUpdateDelayedEventFromWidgetActionResponse extends IUpdateDelayedEventFromWidgetActionRequest { - response: IUpdateDelayedEventFromWidgetResponseData; +export interface IUpdateDelayedEventFromWidgetActionResponse + extends IUpdateDelayedEventFromWidgetActionRequest { + response: IUpdateDelayedEventFromWidgetResponseData; } diff --git a/src/interfaces/UpdateStateAction.ts b/src/interfaces/UpdateStateAction.ts index c497caf..1bbdac9 100644 --- a/src/interfaces/UpdateStateAction.ts +++ b/src/interfaces/UpdateStateAction.ts @@ -20,18 +20,20 @@ import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { IRoomEvent } from "./IRoomEvent"; export interface IUpdateStateToWidgetRequestData extends IWidgetApiRequestData { - state: IRoomEvent[]; + state: IRoomEvent[]; } export interface IUpdateStateToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateState; - data: IUpdateStateToWidgetRequestData; + action: WidgetApiToWidgetAction.UpdateState; + data: IUpdateStateToWidgetRequestData; } -export interface IUpdateStateToWidgetResponseData extends IWidgetApiResponseData { - // nothing +export interface IUpdateStateToWidgetResponseData + extends IWidgetApiResponseData { + // nothing } -export interface IUpdateStateToWidgetActionResponse extends IUpdateStateToWidgetActionRequest { - response: IUpdateStateToWidgetResponseData; +export interface IUpdateStateToWidgetActionResponse + extends IUpdateStateToWidgetActionRequest { + response: IUpdateStateToWidgetResponseData; } diff --git a/src/interfaces/UploadFileAction.ts b/src/interfaces/UploadFileAction.ts index 9d120b6..86d529f 100644 --- a/src/interfaces/UploadFileAction.ts +++ b/src/interfaces/UploadFileAction.ts @@ -18,19 +18,23 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IUploadFileActionFromWidgetRequestData extends IWidgetApiRequestData { - file: XMLHttpRequestBodyInit; +export interface IUploadFileActionFromWidgetRequestData + extends IWidgetApiRequestData { + file: XMLHttpRequestBodyInit; } -export interface IUploadFileActionFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; - data: IUploadFileActionFromWidgetRequestData; +export interface IUploadFileActionFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; + data: IUploadFileActionFromWidgetRequestData; } -export interface IUploadFileActionFromWidgetResponseData extends IWidgetApiResponseData { - content_uri: string; // eslint-disable-line camelcase +export interface IUploadFileActionFromWidgetResponseData + extends IWidgetApiResponseData { + content_uri: string; // eslint-disable-line camelcase } -export interface IUploadFileActionFromWidgetActionResponse extends IUploadFileActionFromWidgetActionRequest { - response: IUploadFileActionFromWidgetResponseData; +export interface IUploadFileActionFromWidgetActionResponse + extends IUploadFileActionFromWidgetActionRequest { + response: IUploadFileActionFromWidgetResponseData; } diff --git a/src/interfaces/UserDirectorySearchAction.ts b/src/interfaces/UserDirectorySearchAction.ts index fb900cc..9747818 100644 --- a/src/interfaces/UserDirectorySearchAction.ts +++ b/src/interfaces/UserDirectorySearchAction.ts @@ -18,25 +18,29 @@ import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; import { IWidgetApiResponseData } from "./IWidgetApiResponse"; import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IUserDirectorySearchFromWidgetRequestData extends IWidgetApiRequestData { - search_term: string; // eslint-disable-line camelcase - limit?: number; +export interface IUserDirectorySearchFromWidgetRequestData + extends IWidgetApiRequestData { + search_term: string; // eslint-disable-line camelcase + limit?: number; } -export interface IUserDirectorySearchFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch; - data: IUserDirectorySearchFromWidgetRequestData; +export interface IUserDirectorySearchFromWidgetActionRequest + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch; + data: IUserDirectorySearchFromWidgetRequestData; } -export interface IUserDirectorySearchFromWidgetResponseData extends IWidgetApiResponseData { - limited: boolean; - results: Array<{ - user_id: string; // eslint-disable-line camelcase - display_name?: string; // eslint-disable-line camelcase - avatar_url?: string; // eslint-disable-line camelcase - }>; +export interface IUserDirectorySearchFromWidgetResponseData + extends IWidgetApiResponseData { + limited: boolean; + results: Array<{ + user_id: string; // eslint-disable-line camelcase + display_name?: string; // eslint-disable-line camelcase + avatar_url?: string; // eslint-disable-line camelcase + }>; } -export interface IUserDirectorySearchFromWidgetActionResponse extends IUserDirectorySearchFromWidgetActionRequest { - response: IUserDirectorySearchFromWidgetResponseData; +export interface IUserDirectorySearchFromWidgetActionResponse + extends IUserDirectorySearchFromWidgetActionRequest { + response: IUserDirectorySearchFromWidgetResponseData; } diff --git a/src/interfaces/VisibilityAction.ts b/src/interfaces/VisibilityAction.ts index 55aa53f..fdb6454 100644 --- a/src/interfaces/VisibilityAction.ts +++ b/src/interfaces/VisibilityAction.ts @@ -19,14 +19,14 @@ import { WidgetApiToWidgetAction } from "./WidgetApiAction"; import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IVisibilityActionRequestData extends IWidgetApiRequestData { - visible: boolean; + visible: boolean; } export interface IVisibilityActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateVisibility; - data: IVisibilityActionRequestData; + action: WidgetApiToWidgetAction.UpdateVisibility; + data: IVisibilityActionRequestData; } export interface IVisibilityActionResponse extends IVisibilityActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index 2f0bcf5..71e12f8 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -15,83 +15,86 @@ */ export enum WidgetApiToWidgetAction { - SupportedApiVersions = "supported_api_versions", - Capabilities = "capabilities", - NotifyCapabilities = "notify_capabilities", - ThemeChange = "theme_change", - LanguageChange = "language_change", - TakeScreenshot = "screenshot", - UpdateVisibility = "visibility", - OpenIDCredentials = "openid_credentials", - WidgetConfig = "widget_config", - CloseModalWidget = "close_modal", - ButtonClicked = "button_clicked", - SendEvent = "send_event", - SendToDevice = "send_to_device", - UpdateState = "update_state", - UpdateTurnServers = "update_turn_servers", + SupportedApiVersions = "supported_api_versions", + Capabilities = "capabilities", + NotifyCapabilities = "notify_capabilities", + ThemeChange = "theme_change", + LanguageChange = "language_change", + TakeScreenshot = "screenshot", + UpdateVisibility = "visibility", + OpenIDCredentials = "openid_credentials", + WidgetConfig = "widget_config", + CloseModalWidget = "close_modal", + ButtonClicked = "button_clicked", + SendEvent = "send_event", + SendToDevice = "send_to_device", + UpdateState = "update_state", + UpdateTurnServers = "update_turn_servers", } export enum WidgetApiFromWidgetAction { - SupportedApiVersions = "supported_api_versions", - ContentLoaded = "content_loaded", - SendSticker = "m.sticker", - UpdateAlwaysOnScreen = "set_always_on_screen", - GetOpenIDCredentials = "get_openid", - CloseModalWidget = "close_modal", - OpenModalWidget = "open_modal", - SetModalButtonEnabled = "set_button_enabled", - SendEvent = "send_event", - SendToDevice = "send_to_device", - WatchTurnServers = "watch_turn_servers", - UnwatchTurnServers = "unwatch_turn_servers", + SupportedApiVersions = "supported_api_versions", + ContentLoaded = "content_loaded", + SendSticker = "m.sticker", + UpdateAlwaysOnScreen = "set_always_on_screen", + GetOpenIDCredentials = "get_openid", + CloseModalWidget = "close_modal", + OpenModalWidget = "open_modal", + SetModalButtonEnabled = "set_button_enabled", + SendEvent = "send_event", + SendToDevice = "send_to_device", + WatchTurnServers = "watch_turn_servers", + UnwatchTurnServers = "unwatch_turn_servers", - BeeperReadRoomAccountData = "com.beeper.read_room_account_data", + BeeperReadRoomAccountData = "com.beeper.read_room_account_data", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2876ReadEvents = "org.matrix.msc2876.read_events", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2876ReadEvents = "org.matrix.msc2876.read_events", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2931Navigate = "org.matrix.msc2931.navigate", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2931Navigate = "org.matrix.msc2931.navigate", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3869ReadRelations = "org.matrix.msc3869.read_relations", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3869ReadRelations = "org.matrix.msc3869.read_relations", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039DownloadFileAction = "org.matrix.msc4039.download_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039DownloadFileAction = "org.matrix.msc4039.download_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } -export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string; +export type WidgetApiAction = + | WidgetApiToWidgetAction + | WidgetApiFromWidgetAction + | string; diff --git a/src/interfaces/WidgetApiDirection.ts b/src/interfaces/WidgetApiDirection.ts index e11e144..6f9b875 100644 --- a/src/interfaces/WidgetApiDirection.ts +++ b/src/interfaces/WidgetApiDirection.ts @@ -15,16 +15,16 @@ */ export enum WidgetApiDirection { - ToWidget = "toWidget", - FromWidget = "fromWidget", + ToWidget = "toWidget", + FromWidget = "fromWidget", } export function invertedDirection(dir: WidgetApiDirection): WidgetApiDirection { - if (dir === WidgetApiDirection.ToWidget) { - return WidgetApiDirection.FromWidget; - } else if (dir === WidgetApiDirection.FromWidget) { - return WidgetApiDirection.ToWidget; - } else { - throw new Error("Invalid direction"); - } + if (dir === WidgetApiDirection.ToWidget) { + return WidgetApiDirection.FromWidget; + } else if (dir === WidgetApiDirection.FromWidget) { + return WidgetApiDirection.ToWidget; + } else { + throw new Error("Invalid direction"); + } } diff --git a/src/interfaces/WidgetConfigAction.ts b/src/interfaces/WidgetConfigAction.ts index b10314c..4989a7b 100644 --- a/src/interfaces/WidgetConfigAction.ts +++ b/src/interfaces/WidgetConfigAction.ts @@ -16,14 +16,17 @@ import { IWidgetApiRequest } from "./IWidgetApiRequest"; import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; +import { + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse"; import { IModalWidgetOpenRequestData } from "./ModalWidgetActions"; export interface IWidgetConfigRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.WidgetConfig; - data: IModalWidgetOpenRequestData; + action: WidgetApiToWidgetAction.WidgetConfig; + data: IModalWidgetOpenRequestData; } export interface IWidgetConfigResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/WidgetKind.ts b/src/interfaces/WidgetKind.ts index 374e198..8c79b22 100644 --- a/src/interfaces/WidgetKind.ts +++ b/src/interfaces/WidgetKind.ts @@ -15,7 +15,7 @@ */ export enum WidgetKind { - Room = "room", - Account = "account", - Modal = "modal", + Room = "room", + Account = "account", + Modal = "modal", } diff --git a/src/interfaces/WidgetType.ts b/src/interfaces/WidgetType.ts index d6b3e33..38ad5c4 100644 --- a/src/interfaces/WidgetType.ts +++ b/src/interfaces/WidgetType.ts @@ -15,9 +15,9 @@ */ export enum MatrixWidgetType { - Custom = "m.custom", - JitsiMeet = "m.jitsi", - Stickerpicker = "m.stickerpicker", + Custom = "m.custom", + JitsiMeet = "m.jitsi", + Stickerpicker = "m.stickerpicker", } export type WidgetType = MatrixWidgetType | string; diff --git a/src/models/Widget.ts b/src/models/Widget.ts index 0b66452..d1f340c 100644 --- a/src/models/Widget.ts +++ b/src/models/Widget.ts @@ -22,88 +22,88 @@ import { ITemplateParams, runTemplate } from ".."; * Represents the barest form of widget. */ export class Widget { - public constructor(private definition: IWidget) { - if (!this.definition) throw new Error("Definition is required"); + public constructor(private definition: IWidget) { + if (!this.definition) throw new Error("Definition is required"); - assertPresent(definition, "id"); - assertPresent(definition, "creatorUserId"); - assertPresent(definition, "type"); - assertPresent(definition, "url"); - } + assertPresent(definition, "id"); + assertPresent(definition, "creatorUserId"); + assertPresent(definition, "type"); + assertPresent(definition, "url"); + } - /** - * The user ID who created the widget. - */ - public get creatorUserId(): string { - return this.definition.creatorUserId; - } + /** + * The user ID who created the widget. + */ + public get creatorUserId(): string { + return this.definition.creatorUserId; + } - /** - * The type of widget. - */ - public get type(): WidgetType { - return this.definition.type; - } + /** + * The type of widget. + */ + public get type(): WidgetType { + return this.definition.type; + } - /** - * The ID of the widget. - */ - public get id(): string { - return this.definition.id; - } + /** + * The ID of the widget. + */ + public get id(): string { + return this.definition.id; + } - /** - * The name of the widget, or null if not set. - */ - public get name(): string | null { - return this.definition.name || null; - } + /** + * The name of the widget, or null if not set. + */ + public get name(): string | null { + return this.definition.name || null; + } - /** - * The title for the widget, or null if not set. - */ - public get title(): string | null { - return this.rawData.title || null; - } + /** + * The title for the widget, or null if not set. + */ + public get title(): string | null { + return this.rawData.title || null; + } - /** - * The templated URL for the widget. - */ - public get templateUrl(): string { - return this.definition.url; - } + /** + * The templated URL for the widget. + */ + public get templateUrl(): string { + return this.definition.url; + } - /** - * The origin for this widget. - */ - public get origin(): string { - return new URL(this.templateUrl).origin; - } + /** + * The origin for this widget. + */ + public get origin(): string { + return new URL(this.templateUrl).origin; + } - /** - * Whether or not the client should wait for the iframe to load. Defaults - * to true. - */ - public get waitForIframeLoad(): boolean { - if (this.definition.waitForIframeLoad === false) return false; - if (this.definition.waitForIframeLoad === true) return true; - return true; // default true - } + /** + * Whether or not the client should wait for the iframe to load. Defaults + * to true. + */ + public get waitForIframeLoad(): boolean { + if (this.definition.waitForIframeLoad === false) return false; + if (this.definition.waitForIframeLoad === true) return true; + return true; // default true + } - /** - * The raw data for the widget. This will always be defined, though - * may be empty. - */ - public get rawData(): IWidgetData { - return this.definition.data || {}; - } + /** + * The raw data for the widget. This will always be defined, though + * may be empty. + */ + public get rawData(): IWidgetData { + return this.definition.data || {}; + } - /** - * Gets a complete widget URL for the client to render. - * @param {ITemplateParams} params The template parameters. - * @returns {string} A templated URL. - */ - public getCompleteUrl(params: ITemplateParams): string { - return runTemplate(this.templateUrl, this.definition, params); - } + /** + * Gets a complete widget URL for the client to render. + * @param {ITemplateParams} params The template parameters. + * @returns {string} A templated URL. + */ + public getCompleteUrl(params: ITemplateParams): string { + return runTemplate(this.templateUrl, this.definition, params); + } } diff --git a/src/models/WidgetEventCapability.ts b/src/models/WidgetEventCapability.ts index 1190606..8655180 100644 --- a/src/models/WidgetEventCapability.ts +++ b/src/models/WidgetEventCapability.ts @@ -17,208 +17,255 @@ import { Capability } from ".."; export enum EventKind { - Event = "event", - State = "state_event", - ToDevice = "to_device", - RoomAccount = "room_account", + Event = "event", + State = "state_event", + ToDevice = "to_device", + RoomAccount = "room_account", } export enum EventDirection { - Send = "send", - Receive = "receive", + Send = "send", + Receive = "receive", } export class WidgetEventCapability { - private constructor( - public readonly direction: EventDirection, - public readonly eventType: string, - public readonly kind: EventKind, - public readonly keyStr: string | null, - public readonly raw: string, - ) {} - - public matchesAsStateEvent(direction: EventDirection, eventType: string, stateKey: string | null): boolean { - if (this.kind !== EventKind.State) return false; // not a state event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - if (this.keyStr === null) return true; // all state keys are allowed - if (this.keyStr === stateKey) return true; // this state key is allowed - - // Default not allowed - return false; + private constructor( + public readonly direction: EventDirection, + public readonly eventType: string, + public readonly kind: EventKind, + public readonly keyStr: string | null, + public readonly raw: string, + ) {} + + public matchesAsStateEvent( + direction: EventDirection, + eventType: string, + stateKey: string | null, + ): boolean { + if (this.kind !== EventKind.State) return false; // not a state event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch + if (this.keyStr === null) return true; // all state keys are allowed + if (this.keyStr === stateKey) return true; // this state key is allowed + + // Default not allowed + return false; + } + + public matchesAsToDeviceEvent( + direction: EventDirection, + eventType: string, + ): boolean { + if (this.kind !== EventKind.ToDevice) return false; // not a to-device event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch + + // Checks passed, the event is allowed + return true; + } + + public matchesAsRoomEvent( + direction: EventDirection, + eventType: string, + msgtype: string | null = null, + ): boolean { + if (this.kind !== EventKind.Event) return false; // not a room event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch + + if (this.eventType === "m.room.message") { + if (this.keyStr === null) return true; // all message types are allowed + if (this.keyStr === msgtype) return true; // this message type is allowed + } else { + return true; // already passed the check for if the event is allowed } - public matchesAsToDeviceEvent(direction: EventDirection, eventType: string): boolean { - if (this.kind !== EventKind.ToDevice) return false; // not a to-device event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - // Checks passed, the event is allowed - return true; - } - - public matchesAsRoomEvent(direction: EventDirection, eventType: string, msgtype: string | null = null): boolean { - if (this.kind !== EventKind.Event) return false; // not a room event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - if (this.eventType === "m.room.message") { - if (this.keyStr === null) return true; // all message types are allowed - if (this.keyStr === msgtype) return true; // this message type is allowed - } else { - return true; // already passed the check for if the event is allowed - } - - // Default not allowed - return false; - } - - public matchesAsRoomAccountData(direction: EventDirection, eventType: string): boolean { - if (this.kind !== EventKind.RoomAccount) return false; // not room account data - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - // Checks passed, the event is allowed - return true; - } - - public static forStateEvent( - direction: EventDirection, - eventType: string, - stateKey?: string, - ): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - eventType = eventType.replace(/#/g, "\\#"); - stateKey = stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ""; - const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forToDeviceEvent(direction: EventDirection, eventType: string): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/56 - const str = `org.matrix.msc3819.${direction}.to_device:${eventType}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomEvent(direction: EventDirection, eventType: string): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - const str = `org.matrix.msc2762.${direction}.event:${eventType}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomMessageEvent(direction: EventDirection, msgtype?: string): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - msgtype = msgtype === null || msgtype === undefined ? "" : msgtype; - const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomAccountData(direction: EventDirection, eventType: string): WidgetEventCapability { - const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}`; - - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - /** - * Parses a capabilities request to find all the event capability requests. - * @param {Iterable} capabilities The capabilities requested/to parse. - * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. - */ - public static findEventCapabilities(capabilities: Iterable): WidgetEventCapability[] { - const parsed: WidgetEventCapability[] = []; - for (const cap of capabilities) { - let direction: EventDirection | null = null; - let eventSegment: string | undefined; - let kind: EventKind | null = null; - - // TODO: Enable support for m.* namespace once the MSCs land. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - // https://github.com/matrix-org/matrix-widget-api/issues/56 - - if (cap.startsWith("org.matrix.msc2762.send.event:")) { - direction = EventDirection.Send; - kind = EventKind.Event; - eventSegment = cap.substring("org.matrix.msc2762.send.event:".length); - } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { - direction = EventDirection.Send; - kind = EventKind.State; - eventSegment = cap.substring("org.matrix.msc2762.send.state_event:".length); - } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { - direction = EventDirection.Send; - kind = EventKind.ToDevice; - eventSegment = cap.substring("org.matrix.msc3819.send.to_device:".length); - } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { - direction = EventDirection.Receive; - kind = EventKind.Event; - eventSegment = cap.substring("org.matrix.msc2762.receive.event:".length); - } else if (cap.startsWith("org.matrix.msc2762.receive.state_event:")) { - direction = EventDirection.Receive; - kind = EventKind.State; - eventSegment = cap.substring("org.matrix.msc2762.receive.state_event:".length); - } else if (cap.startsWith("org.matrix.msc3819.receive.to_device:")) { - direction = EventDirection.Receive; - kind = EventKind.ToDevice; - eventSegment = cap.substring("org.matrix.msc3819.receive.to_device:".length); - } else if (cap.startsWith("com.beeper.capabilities.receive.room_account_data:")) { - direction = EventDirection.Receive; - kind = EventKind.RoomAccount; - eventSegment = cap.substring("com.beeper.capabilities.receive.room_account_data:".length); - } - - if (direction === null || kind === null || eventSegment === undefined) continue; - - // The capability uses `#` as a separator between event type and state key/msgtype, - // so we split on that. However, a # is also valid in either one of those so we - // join accordingly. - // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". - const expectingKeyStr = eventSegment.startsWith("m.room.message#") || kind === EventKind.State; - let keyStr: string | null = null; - if (eventSegment.includes("#") && expectingKeyStr) { - // Dev note: regex is difficult to write, so instead the rules are manually written - // out. This is probably just as understandable as a boring regex though, so win-win? - - // Test cases: - // str eventSegment keyStr - // ------------------------------------------------------------- - // m.room.message# m.room.message - // m.room.message#test m.room.message test - // m.room.message\# m.room.message# test - // m.room.message##test m.room.message #test - // m.room.message\##test m.room.message# test - // m.room.message\\##test m.room.message\# test - // m.room.message\\###test m.room.message\# #test - - // First step: explode the string - const parts = eventSegment.split("#"); - - // To form the eventSegment, we'll keep finding parts of the exploded string until - // there's one that doesn't end with the escape character (\). We'll then join those - // segments together with the exploding character. We have to remember to consume the - // escape character as well. - const idx = parts.findIndex((p) => !p.endsWith("\\")); - eventSegment = parts - .slice(0, idx + 1) - .map((p) => (p.endsWith("\\") ? p.substring(0, p.length - 1) : p)) - .join("#"); - - // The keyStr is whatever is left over. - keyStr = parts.slice(idx + 1).join("#"); - } - - parsed.push(new WidgetEventCapability(direction, eventSegment, kind, keyStr, cap)); - } - return parsed; + // Default not allowed + return false; + } + + public matchesAsRoomAccountData( + direction: EventDirection, + eventType: string, + ): boolean { + if (this.kind !== EventKind.RoomAccount) return false; // not room account data + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch + + // Checks passed, the event is allowed + return true; + } + + public static forStateEvent( + direction: EventDirection, + eventType: string, + stateKey?: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + eventType = eventType.replace(/#/g, "\\#"); + stateKey = + stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ""; + const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}`; + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + + public static forToDeviceEvent( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/56 + const str = `org.matrix.msc3819.${direction}.to_device:${eventType}`; + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + + public static forRoomEvent( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + const str = `org.matrix.msc2762.${direction}.event:${eventType}`; + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + + public static forRoomMessageEvent( + direction: EventDirection, + msgtype?: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + msgtype = msgtype === null || msgtype === undefined ? "" : msgtype; + const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}`; + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + + public static forRoomAccountData( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}`; + + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + + /** + * Parses a capabilities request to find all the event capability requests. + * @param {Iterable} capabilities The capabilities requested/to parse. + * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. + */ + public static findEventCapabilities( + capabilities: Iterable, + ): WidgetEventCapability[] { + const parsed: WidgetEventCapability[] = []; + for (const cap of capabilities) { + let direction: EventDirection | null = null; + let eventSegment: string | undefined; + let kind: EventKind | null = null; + + // TODO: Enable support for m.* namespace once the MSCs land. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + // https://github.com/matrix-org/matrix-widget-api/issues/56 + + if (cap.startsWith("org.matrix.msc2762.send.event:")) { + direction = EventDirection.Send; + kind = EventKind.Event; + eventSegment = cap.substring("org.matrix.msc2762.send.event:".length); + } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { + direction = EventDirection.Send; + kind = EventKind.State; + eventSegment = cap.substring( + "org.matrix.msc2762.send.state_event:".length, + ); + } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { + direction = EventDirection.Send; + kind = EventKind.ToDevice; + eventSegment = cap.substring( + "org.matrix.msc3819.send.to_device:".length, + ); + } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { + direction = EventDirection.Receive; + kind = EventKind.Event; + eventSegment = cap.substring( + "org.matrix.msc2762.receive.event:".length, + ); + } else if (cap.startsWith("org.matrix.msc2762.receive.state_event:")) { + direction = EventDirection.Receive; + kind = EventKind.State; + eventSegment = cap.substring( + "org.matrix.msc2762.receive.state_event:".length, + ); + } else if (cap.startsWith("org.matrix.msc3819.receive.to_device:")) { + direction = EventDirection.Receive; + kind = EventKind.ToDevice; + eventSegment = cap.substring( + "org.matrix.msc3819.receive.to_device:".length, + ); + } else if ( + cap.startsWith("com.beeper.capabilities.receive.room_account_data:") + ) { + direction = EventDirection.Receive; + kind = EventKind.RoomAccount; + eventSegment = cap.substring( + "com.beeper.capabilities.receive.room_account_data:".length, + ); + } + + if (direction === null || kind === null || eventSegment === undefined) + continue; + + // The capability uses `#` as a separator between event type and state key/msgtype, + // so we split on that. However, a # is also valid in either one of those so we + // join accordingly. + // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". + const expectingKeyStr = + eventSegment.startsWith("m.room.message#") || kind === EventKind.State; + let keyStr: string | null = null; + if (eventSegment.includes("#") && expectingKeyStr) { + // Dev note: regex is difficult to write, so instead the rules are manually written + // out. This is probably just as understandable as a boring regex though, so win-win? + + // Test cases: + // str eventSegment keyStr + // ------------------------------------------------------------- + // m.room.message# m.room.message + // m.room.message#test m.room.message test + // m.room.message\# m.room.message# test + // m.room.message##test m.room.message #test + // m.room.message\##test m.room.message# test + // m.room.message\\##test m.room.message\# test + // m.room.message\\###test m.room.message\# #test + + // First step: explode the string + const parts = eventSegment.split("#"); + + // To form the eventSegment, we'll keep finding parts of the exploded string until + // there's one that doesn't end with the escape character (\). We'll then join those + // segments together with the exploding character. We have to remember to consume the + // escape character as well. + const idx = parts.findIndex((p) => !p.endsWith("\\")); + eventSegment = parts + .slice(0, idx + 1) + .map((p) => (p.endsWith("\\") ? p.substring(0, p.length - 1) : p)) + .join("#"); + + // The keyStr is whatever is left over. + keyStr = parts.slice(idx + 1).join("#"); + } + + parsed.push( + new WidgetEventCapability(direction, eventSegment, kind, keyStr, cap), + ); } + return parsed; + } } diff --git a/src/models/WidgetParser.ts b/src/models/WidgetParser.ts index 07ced72..bf82365 100644 --- a/src/models/WidgetParser.ts +++ b/src/models/WidgetParser.ts @@ -19,129 +19,138 @@ import { IWidget } from ".."; import { isValidUrl } from "./validation/url"; export interface IStateEvent { - event_id: string; // eslint-disable-line camelcase - room_id: string; // eslint-disable-line camelcase - type: string; - sender: string; - origin_server_ts: number; // eslint-disable-line camelcase - unsigned?: unknown; - content: unknown; - state_key: string; // eslint-disable-line camelcase + event_id: string; // eslint-disable-line camelcase + room_id: string; // eslint-disable-line camelcase + type: string; + sender: string; + origin_server_ts: number; // eslint-disable-line camelcase + unsigned?: unknown; + content: unknown; + state_key: string; // eslint-disable-line camelcase } export interface IAccountDataWidgets { - [widgetId: string]: { - type: "m.widget"; - // the state_key is also the widget's ID - state_key: string; // eslint-disable-line camelcase - sender: string; // current user's ID - content: IWidget; - id?: string; // off-spec, but possible - }; + [widgetId: string]: { + type: "m.widget"; + // the state_key is also the widget's ID + state_key: string; // eslint-disable-line camelcase + sender: string; // current user's ID + content: IWidget; + id?: string; // off-spec, but possible + }; } export class WidgetParser { - private constructor() { - // private constructor because this is a util class + private constructor() { + // private constructor because this is a util class + } + + /** + * Parses widgets from the "m.widgets" account data event. This will always + * return an array, though may be empty if no valid widgets were found. + * @param {IAccountDataWidgets} content The content of the "m.widgets" account data. + * @returns {Widget[]} The widgets in account data, or an empty array. + */ + public static parseAccountData(content: IAccountDataWidgets): Widget[] { + if (!content) return []; + + const result: Widget[] = []; + for (const widgetId of Object.keys(content)) { + const roughWidget = content[widgetId]; + if (!roughWidget) continue; + if ( + roughWidget.type !== "m.widget" && + roughWidget.type !== "im.vector.modular.widgets" + ) + continue; + if (!roughWidget.sender) continue; + + const probableWidgetId = roughWidget.state_key || roughWidget.id; + if (probableWidgetId !== widgetId) continue; + + const asStateEvent: IStateEvent = { + content: roughWidget.content, + sender: roughWidget.sender, + type: "m.widget", + state_key: widgetId, + event_id: "$example", + room_id: "!example", + origin_server_ts: 1, + }; + + const widget = WidgetParser.parseRoomWidget(asStateEvent); + if (widget) result.push(widget); } - /** - * Parses widgets from the "m.widgets" account data event. This will always - * return an array, though may be empty if no valid widgets were found. - * @param {IAccountDataWidgets} content The content of the "m.widgets" account data. - * @returns {Widget[]} The widgets in account data, or an empty array. - */ - public static parseAccountData(content: IAccountDataWidgets): Widget[] { - if (!content) return []; - - const result: Widget[] = []; - for (const widgetId of Object.keys(content)) { - const roughWidget = content[widgetId]; - if (!roughWidget) continue; - if (roughWidget.type !== "m.widget" && roughWidget.type !== "im.vector.modular.widgets") continue; - if (!roughWidget.sender) continue; - - const probableWidgetId = roughWidget.state_key || roughWidget.id; - if (probableWidgetId !== widgetId) continue; - - const asStateEvent: IStateEvent = { - content: roughWidget.content, - sender: roughWidget.sender, - type: "m.widget", - state_key: widgetId, - event_id: "$example", - room_id: "!example", - origin_server_ts: 1, - }; - - const widget = WidgetParser.parseRoomWidget(asStateEvent); - if (widget) result.push(widget); - } - - return result; + return result; + } + + /** + * Parses all the widgets possible in the given array. This will always return + * an array, though may be empty if no widgets could be parsed. + * @param {IStateEvent[]} currentState The room state to parse. + * @returns {Widget[]} The widgets in the state, or an empty array. + */ + public static parseWidgetsFromRoomState( + currentState: IStateEvent[], + ): Widget[] { + if (!currentState) return []; + const result: Widget[] = []; + for (const state of currentState) { + const widget = WidgetParser.parseRoomWidget(state); + if (widget) result.push(widget); } - - /** - * Parses all the widgets possible in the given array. This will always return - * an array, though may be empty if no widgets could be parsed. - * @param {IStateEvent[]} currentState The room state to parse. - * @returns {Widget[]} The widgets in the state, or an empty array. - */ - public static parseWidgetsFromRoomState(currentState: IStateEvent[]): Widget[] { - if (!currentState) return []; - const result: Widget[] = []; - for (const state of currentState) { - const widget = WidgetParser.parseRoomWidget(state); - if (widget) result.push(widget); - } - return result; + return result; + } + + /** + * Parses a state event into a widget. If the state event does not represent + * a widget (wrong event type, invalid widget, etc) then null is returned. + * @param {IStateEvent} stateEvent The state event. + * @returns {Widget|null} The widget, or null if invalid + */ + public static parseRoomWidget(stateEvent: IStateEvent): Widget | null { + if (!stateEvent) return null; + + // TODO: [Legacy] Remove legacy support + if ( + stateEvent.type !== "m.widget" && + stateEvent.type !== "im.vector.modular.widgets" + ) { + return null; } - /** - * Parses a state event into a widget. If the state event does not represent - * a widget (wrong event type, invalid widget, etc) then null is returned. - * @param {IStateEvent} stateEvent The state event. - * @returns {Widget|null} The widget, or null if invalid - */ - public static parseRoomWidget(stateEvent: IStateEvent): Widget | null { - if (!stateEvent) return null; - - // TODO: [Legacy] Remove legacy support - if (stateEvent.type !== "m.widget" && stateEvent.type !== "im.vector.modular.widgets") { - return null; - } - - // Dev note: Throughout this function we have null safety to ensure that - // if the caller did not supply something useful that we don't error. This - // is done against the requirements of the interface because not everyone - // will have an interface to validate against. - - const content = (stateEvent.content as IWidget) || {}; - - // Form our best approximation of a widget with the information we have - const estimatedWidget: IWidget = { - id: stateEvent.state_key, - creatorUserId: content["creatorUserId"] || stateEvent.sender, - name: content["name"], - type: content["type"], - url: content["url"], - waitForIframeLoad: content["waitForIframeLoad"], - data: content["data"], - }; - - // Finally, process that widget - return WidgetParser.processEstimatedWidget(estimatedWidget); - } + // Dev note: Throughout this function we have null safety to ensure that + // if the caller did not supply something useful that we don't error. This + // is done against the requirements of the interface because not everyone + // will have an interface to validate against. + + const content = (stateEvent.content as IWidget) || {}; + + // Form our best approximation of a widget with the information we have + const estimatedWidget: IWidget = { + id: stateEvent.state_key, + creatorUserId: content["creatorUserId"] || stateEvent.sender, + name: content["name"], + type: content["type"], + url: content["url"], + waitForIframeLoad: content["waitForIframeLoad"], + data: content["data"], + }; - private static processEstimatedWidget(widget: IWidget): Widget | null { - // Validate that the widget has the best chance of passing as a widget - if (!widget.id || !widget.creatorUserId || !widget.type) { - return null; - } - if (!isValidUrl(widget.url)) { - return null; - } - // TODO: Validate data for known widget types - return new Widget(widget); + // Finally, process that widget + return WidgetParser.processEstimatedWidget(estimatedWidget); + } + + private static processEstimatedWidget(widget: IWidget): Widget | null { + // Validate that the widget has the best chance of passing as a widget + if (!widget.id || !widget.creatorUserId || !widget.type) { + return null; + } + if (!isValidUrl(widget.url)) { + return null; } + // TODO: Validate data for known widget types + return new Widget(widget); + } } diff --git a/src/models/validation/url.ts b/src/models/validation/url.ts index c56a9c6..4f0480a 100644 --- a/src/models/validation/url.ts +++ b/src/models/validation/url.ts @@ -15,18 +15,18 @@ */ export function isValidUrl(val: string): boolean { - if (!val) return false; // easy: not valid if not present + if (!val) return false; // easy: not valid if not present - try { - const parsed = new URL(val); - if (parsed.protocol !== "http" && parsed.protocol !== "https") { - return false; - } - return true; - } catch (e) { - if (e instanceof TypeError) { - return false; - } - throw e; + try { + const parsed = new URL(val); + if (parsed.protocol !== "http" && parsed.protocol !== "https") { + return false; } + return true; + } catch (e) { + if (e instanceof TypeError) { + return false; + } + throw e; + } } diff --git a/src/models/validation/utils.ts b/src/models/validation/utils.ts index 5572c0f..52efb16 100644 --- a/src/models/validation/utils.ts +++ b/src/models/validation/utils.ts @@ -15,8 +15,11 @@ */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function assertPresent>(obj: O, key: keyof O): void { - if (!obj[key]) { - throw new Error(`${String(key)} is required`); - } +export function assertPresent>( + obj: O, + key: keyof O, +): void { + if (!obj[key]) { + throw new Error(`${String(key)} is required`); + } } diff --git a/src/templating/url-template.ts b/src/templating/url-template.ts index b700a9b..2861149 100644 --- a/src/templating/url-template.ts +++ b/src/templating/url-template.ts @@ -17,55 +17,59 @@ import { IWidget } from ".."; export interface ITemplateParams { - widgetRoomId?: string; - currentUserId: string; - userDisplayName?: string; - userHttpAvatarUrl?: string; - clientId?: string; - clientTheme?: string; - clientLanguage?: string; - deviceId?: string; - baseUrl?: string; + widgetRoomId?: string; + currentUserId: string; + userDisplayName?: string; + userHttpAvatarUrl?: string; + clientId?: string; + clientTheme?: string; + clientLanguage?: string; + deviceId?: string; + baseUrl?: string; } -export function runTemplate(url: string, widget: IWidget, params: ITemplateParams): string { - // Always apply the supplied params over top of data to ensure the data can't lie about them. - const variables = Object.assign({}, widget.data, { - "matrix_room_id": params.widgetRoomId || "", - "matrix_user_id": params.currentUserId, - "matrix_display_name": params.userDisplayName || params.currentUserId, - "matrix_avatar_url": params.userHttpAvatarUrl || "", - "matrix_widget_id": widget.id, +export function runTemplate( + url: string, + widget: IWidget, + params: ITemplateParams, +): string { + // Always apply the supplied params over top of data to ensure the data can't lie about them. + const variables = Object.assign({}, widget.data, { + matrix_room_id: params.widgetRoomId || "", + matrix_user_id: params.currentUserId, + matrix_display_name: params.userDisplayName || params.currentUserId, + matrix_avatar_url: params.userHttpAvatarUrl || "", + matrix_widget_id: widget.id, - // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) - "org.matrix.msc2873.client_id": params.clientId || "", - "org.matrix.msc2873.client_theme": params.clientTheme || "", - "org.matrix.msc2873.client_language": params.clientLanguage || "", + // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) + "org.matrix.msc2873.client_id": params.clientId || "", + "org.matrix.msc2873.client_theme": params.clientTheme || "", + "org.matrix.msc2873.client_language": params.clientLanguage || "", - // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) - "org.matrix.msc3819.matrix_device_id": params.deviceId || "", + // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) + "org.matrix.msc3819.matrix_device_id": params.deviceId || "", - // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) - "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", - }); - let result = url; - for (const key of Object.keys(variables)) { - // Regex escape from https://stackoverflow.com/a/6969486/7037379 - const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - const rexp = new RegExp(pattern, "g"); + // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) + "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", + }); + let result = url; + for (const key of Object.keys(variables)) { + // Regex escape from https://stackoverflow.com/a/6969486/7037379 + const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + const rexp = new RegExp(pattern, "g"); - // This is technically not what we're supposed to do for a couple of reasons: - // 1. We are assuming that there won't later be a $key match after we replace a variable. - // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). - result = result.replace(rexp, encodeURIComponent(toString(variables[key]))); - } - return result; + // This is technically not what we're supposed to do for a couple of reasons: + // 1. We are assuming that there won't later be a $key match after we replace a variable. + // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). + result = result.replace(rexp, encodeURIComponent(toString(variables[key]))); + } + return result; } export function toString(a: unknown): string { - if (a === null || a === undefined) { - return `${a}`; - } - // eslint-disable-next-line @typescript-eslint/no-base-to-string - return String(a); + if (a === null || a === undefined) { + return `${a}`; + } + // eslint-disable-next-line @typescript-eslint/no-base-to-string + return String(a); } diff --git a/src/transport/ITransport.ts b/src/transport/ITransport.ts index 3446e6a..b6dda14 100644 --- a/src/transport/ITransport.ts +++ b/src/transport/ITransport.ts @@ -17,12 +17,12 @@ import { EventEmitter } from "events"; import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - WidgetApiAction, + IWidgetApiAcknowledgeResponseData, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + WidgetApiAction, } from ".."; /** @@ -31,79 +31,85 @@ import { * of the IWidgetApiRequest. */ export interface ITransport extends EventEmitter { - /** - * True if the transport is ready to start sending, false otherwise. - */ - readonly ready: boolean; + /** + * True if the transport is ready to start sending, false otherwise. + */ + readonly ready: boolean; - /** - * The widget ID, if known. If not known, null. - */ - readonly widgetId: string | null; + /** + * The widget ID, if known. If not known, null. + */ + readonly widgetId: string | null; - /** - * If true, the transport will refuse requests from origins other than the - * widget's current origin. This is intended to be used only by widgets which - * need excess security. - */ - strictOriginCheck: boolean; + /** + * If true, the transport will refuse requests from origins other than the + * widget's current origin. This is intended to be used only by widgets which + * need excess security. + */ + strictOriginCheck: boolean; - /** - * The origin the transport should be replying/sending to. If not known, leave - * null. - */ - targetOrigin: string | null; + /** + * The origin the transport should be replying/sending to. If not known, leave + * null. + */ + targetOrigin: string | null; - /** - * The number of seconds an outbound request is allowed to take before it - * times out. - */ - timeoutSeconds: number; + /** + * The number of seconds an outbound request is allowed to take before it + * times out. + */ + timeoutSeconds: number; - /** - * Starts the transport for listening - */ - start(): void; + /** + * Starts the transport for listening + */ + start(): void; - /** - * Stops the transport. It cannot be re-started. - */ - stop(): void; + /** + * Stops the transport. It cannot be re-started. + */ + stop(): void; - /** - * Sends a request to the remote end. - * @param action The action to send. - * @param data The request data. - * @returns A promise which resolves to the remote end's response. - * @throws {Error} if the request failed with a generic error. - * @throws {WidgetApiResponseError} if the request failed with error details - * that can be communicated to the Widget API. - */ - send( - action: WidgetApiAction, - data: T, - ): Promise; + /** + * Sends a request to the remote end. + * @param action The action to send. + * @param data The request data. + * @returns A promise which resolves to the remote end's response. + * @throws {Error} if the request failed with a generic error. + * @throws {WidgetApiResponseError} if the request failed with error details + * that can be communicated to the Widget API. + */ + send< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData, + >( + action: WidgetApiAction, + data: T, + ): Promise; - /** - * Sends a request to the remote end. This is similar to the send() function - * however this version returns the full response rather than just the response - * data. - * @param {WidgetApiAction} action The action to send. - * @param {IWidgetApiRequestData} data The request data. - * @returns {Promise} A promise which resolves to the remote end's response - * @throws {Error} if the request failed with a generic error. - * @throws {WidgetApiResponseError} if the request failed with error details - * that can be communicated to the Widget API. - */ - sendComplete( - action: WidgetApiAction, - data: T, - ): Promise; + /** + * Sends a request to the remote end. This is similar to the send() function + * however this version returns the full response rather than just the response + * data. + * @param {WidgetApiAction} action The action to send. + * @param {IWidgetApiRequestData} data The request data. + * @returns {Promise} A promise which resolves to the remote end's response + * @throws {Error} if the request failed with a generic error. + * @throws {WidgetApiResponseError} if the request failed with error details + * that can be communicated to the Widget API. + */ + sendComplete( + action: WidgetApiAction, + data: T, + ): Promise; - /** - * Replies to a request. - * @param {IWidgetApiRequest} request The request to reply to. - * @param {IWidgetApiResponseData} responseData The response data to reply with. - */ - reply(request: IWidgetApiRequest, responseData: T): void; + /** + * Replies to a request. + * @param {IWidgetApiRequest} request The request to reply to. + * @param {IWidgetApiResponseData} responseData The response data to reply with. + */ + reply( + request: IWidgetApiRequest, + responseData: T, + ): void; } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 4589735..733825d 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -18,186 +18,199 @@ import { EventEmitter } from "events"; import { ITransport } from "./ITransport"; import { - invertedDirection, - isErrorResponse, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - WidgetApiResponseError, - WidgetApiAction, - WidgetApiDirection, - WidgetApiToWidgetAction, + invertedDirection, + isErrorResponse, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + WidgetApiResponseError, + WidgetApiAction, + WidgetApiDirection, + WidgetApiToWidgetAction, } from ".."; interface IOutboundRequest { - request: IWidgetApiRequest; - resolve: (response: IWidgetApiResponse) => void; - reject: (err: Error) => void; + request: IWidgetApiRequest; + resolve: (response: IWidgetApiResponse) => void; + reject: (err: Error) => void; } /** * Transport for the Widget API over postMessage. */ export class PostmessageTransport extends EventEmitter implements ITransport { - public strictOriginCheck = false; - public targetOrigin = "*"; - public timeoutSeconds = 10; - - private _ready = false; - private _widgetId: string | null = null; - private outboundRequests = new Map(); - private stopController = new AbortController(); - - public get ready(): boolean { - return this._ready; - } - - public get widgetId(): string | null { - return this._widgetId || null; - } - - public constructor( - private sendDirection: WidgetApiDirection, - private initialWidgetId: string | null, - private transportWindow: Window, - private inboundWindow: Window, - ) { - super(); - this._widgetId = initialWidgetId; - } - - private get nextRequestId(): string { - const idBase = `widgetapi-${Date.now()}`; - let index = 0; - let id = idBase; - while (this.outboundRequests.has(id)) { - id = `${idBase}-${index++}`; - } - - // reserve the ID - this.outboundRequests.set(id, null); - - return id; + public strictOriginCheck = false; + public targetOrigin = "*"; + public timeoutSeconds = 10; + + private _ready = false; + private _widgetId: string | null = null; + private outboundRequests = new Map(); + private stopController = new AbortController(); + + public get ready(): boolean { + return this._ready; + } + + public get widgetId(): string | null { + return this._widgetId || null; + } + + public constructor( + private sendDirection: WidgetApiDirection, + private initialWidgetId: string | null, + private transportWindow: Window, + private inboundWindow: Window, + ) { + super(); + this._widgetId = initialWidgetId; + } + + private get nextRequestId(): string { + const idBase = `widgetapi-${Date.now()}`; + let index = 0; + let id = idBase; + while (this.outboundRequests.has(id)) { + id = `${idBase}-${index++}`; } - private sendInternal(message: IWidgetApiRequest | IWidgetApiResponse): void { - console.log(`[PostmessageTransport] Sending object to ${this.targetOrigin}: `, message); - this.transportWindow.postMessage(message, this.targetOrigin); + // reserve the ID + this.outboundRequests.set(id, null); + + return id; + } + + private sendInternal(message: IWidgetApiRequest | IWidgetApiResponse): void { + console.log( + `[PostmessageTransport] Sending object to ${this.targetOrigin}: `, + message, + ); + this.transportWindow.postMessage(message, this.targetOrigin); + } + + public reply( + request: IWidgetApiRequest, + responseData: T, + ): void { + return this.sendInternal({ + ...request, + response: responseData, + }); + } + + public send< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponseData, + >(action: WidgetApiAction, data: T): Promise { + return this.sendComplete(action, data).then((r) => r.response); + } + + public sendComplete< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponse, + >(action: WidgetApiAction, data: T): Promise { + if (!this.ready || !this.widgetId) { + return Promise.reject(new Error("Not ready or unknown widget ID")); } - - public reply(request: IWidgetApiRequest, responseData: T): void { - return this.sendInternal({ - ...request, - response: responseData, - }); - } - - public send( - action: WidgetApiAction, - data: T, - ): Promise { - return this.sendComplete(action, data).then((r) => r.response); - } - - public sendComplete( - action: WidgetApiAction, - data: T, - ): Promise { - if (!this.ready || !this.widgetId) { - return Promise.reject(new Error("Not ready or unknown widget ID")); - } - const request: IWidgetApiRequest = { - api: this.sendDirection, - widgetId: this.widgetId, - requestId: this.nextRequestId, - action: action, - data: data, - }; - if (action === WidgetApiToWidgetAction.UpdateVisibility) { - request["visible"] = data["visible"]; - } - return new Promise((prResolve, prReject) => { - const resolve = (response: IWidgetApiResponse): void => { - cleanUp(); - prResolve(response); - }; - const reject = (err: Error): void => { - cleanUp(); - prReject(err); - }; - - const timerId = setTimeout(() => reject(new Error("Request timed out")), (this.timeoutSeconds || 1) * 1000); - - const onStop = (): void => reject(new Error("Transport stopped")); - this.stopController.signal.addEventListener("abort", onStop); - - const cleanUp = (): void => { - this.outboundRequests.delete(request.requestId); - clearTimeout(timerId); - this.stopController.signal.removeEventListener("abort", onStop); - }; - - this.outboundRequests.set(request.requestId, { request, resolve, reject }); - this.sendInternal(request); - }); + const request: IWidgetApiRequest = { + api: this.sendDirection, + widgetId: this.widgetId, + requestId: this.nextRequestId, + action: action, + data: data, + }; + if (action === WidgetApiToWidgetAction.UpdateVisibility) { + request["visible"] = data["visible"]; } - - public start(): void { - this.inboundWindow.addEventListener("message", (ev: MessageEvent) => { - this.handleMessage(ev); - }); - this._ready = true; - } - - public stop(): void { - this._ready = false; - this.stopController.abort(); + return new Promise((prResolve, prReject) => { + const resolve = (response: IWidgetApiResponse): void => { + cleanUp(); + prResolve(response); + }; + const reject = (err: Error): void => { + cleanUp(); + prReject(err); + }; + + const timerId = setTimeout( + () => reject(new Error("Request timed out")), + (this.timeoutSeconds || 1) * 1000, + ); + + const onStop = (): void => reject(new Error("Transport stopped")); + this.stopController.signal.addEventListener("abort", onStop); + + const cleanUp = (): void => { + this.outboundRequests.delete(request.requestId); + clearTimeout(timerId); + this.stopController.signal.removeEventListener("abort", onStop); + }; + + this.outboundRequests.set(request.requestId, { + request, + resolve, + reject, + }); + this.sendInternal(request); + }); + } + + public start(): void { + this.inboundWindow.addEventListener("message", (ev: MessageEvent) => { + this.handleMessage(ev); + }); + this._ready = true; + } + + public stop(): void { + this._ready = false; + this.stopController.abort(); + } + + private handleMessage(ev: MessageEvent): void { + if (this.stopController.signal.aborted) return; + if (!ev.data) return; // invalid event + + if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin + + // treat the message as a response first, then downgrade to a request + const response = ev.data; + if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response + + if (!response.response) { + // it's a request + const request = response; + if (request.api !== invertedDirection(this.sendDirection)) return; // wrong direction + this.handleRequest(request); + } else { + // it's a response + if (response.api !== this.sendDirection) return; // wrong direction + this.handleResponse(response); } + } - private handleMessage(ev: MessageEvent): void { - if (this.stopController.signal.aborted) return; - if (!ev.data) return; // invalid event - - if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin - - // treat the message as a response first, then downgrade to a request - const response = ev.data; - if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response - - if (!response.response) { - // it's a request - const request = response; - if (request.api !== invertedDirection(this.sendDirection)) return; // wrong direction - this.handleRequest(request); - } else { - // it's a response - if (response.api !== this.sendDirection) return; // wrong direction - this.handleResponse(response); - } + private handleRequest(request: IWidgetApiRequest): void { + if (this.widgetId) { + if (this.widgetId !== request.widgetId) return; // wrong widget + } else { + this._widgetId = request.widgetId; } - private handleRequest(request: IWidgetApiRequest): void { - if (this.widgetId) { - if (this.widgetId !== request.widgetId) return; // wrong widget - } else { - this._widgetId = request.widgetId; - } - - this.emit("message", new CustomEvent("message", { detail: request })); - } + this.emit("message", new CustomEvent("message", { detail: request })); + } - private handleResponse(response: IWidgetApiResponse): void { - if (response.widgetId !== this.widgetId) return; // wrong widget + private handleResponse(response: IWidgetApiResponse): void { + if (response.widgetId !== this.widgetId) return; // wrong widget - const req = this.outboundRequests.get(response.requestId); - if (!req) return; // response to an unknown request + const req = this.outboundRequests.get(response.requestId); + if (!req) return; // response to an unknown request - if (isErrorResponse(response.response)) { - const { message, ...data } = response.response.error; - req.reject(new WidgetApiResponseError(message, data)); - } else { - req.resolve(response); - } + if (isErrorResponse(response.response)) { + const { message, ...data } = response.response.error; + req.reject(new WidgetApiResponseError(message, data)); + } else { + req.resolve(response); } + } } diff --git a/src/util/SimpleObservable.ts b/src/util/SimpleObservable.ts index 5108247..cffa861 100644 --- a/src/util/SimpleObservable.ts +++ b/src/util/SimpleObservable.ts @@ -17,23 +17,23 @@ export type ObservableFunction = (val: T) => void; export class SimpleObservable { - private listeners: ObservableFunction[] = []; + private listeners: ObservableFunction[] = []; - public constructor(initialFn?: ObservableFunction) { - if (initialFn) this.listeners.push(initialFn); - } + public constructor(initialFn?: ObservableFunction) { + if (initialFn) this.listeners.push(initialFn); + } - public onUpdate(fn: ObservableFunction): void { - this.listeners.push(fn); - } + public onUpdate(fn: ObservableFunction): void { + this.listeners.push(fn); + } - public update(val: T): void { - for (const listener of this.listeners) { - listener(val); - } + public update(val: T): void { + for (const listener of this.listeners) { + listener(val); } + } - public close(): void { - this.listeners = []; // reset - } + public close(): void { + this.listeners = []; // reset + } } diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 0a261f2..dff644a 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -26,25 +26,28 @@ import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "../src/interfaces/WidgetApiAction"; import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; import { Widget } from "../src/models/Widget"; import { PostmessageTransport } from "../src/transport/PostmessageTransport"; import { - IDownloadFileActionFromWidgetActionRequest, - IGetOpenIDActionRequest, - IMatrixApiError, - INavigateActionRequest, - IReadEventFromWidgetActionRequest, - ISendEventFromWidgetActionRequest, - ISendToDeviceFromWidgetActionRequest, - IUpdateDelayedEventFromWidgetActionRequest, - IUploadFileActionFromWidgetActionRequest, - IWidgetApiErrorResponseDataDetails, - OpenIDRequestState, - SimpleObservable, - Symbols, - UpdateDelayedEventAction, + IDownloadFileActionFromWidgetActionRequest, + IGetOpenIDActionRequest, + IMatrixApiError, + INavigateActionRequest, + IReadEventFromWidgetActionRequest, + ISendEventFromWidgetActionRequest, + ISendToDeviceFromWidgetActionRequest, + IUpdateDelayedEventFromWidgetActionRequest, + IUploadFileActionFromWidgetActionRequest, + IWidgetApiErrorResponseDataDetails, + OpenIDRequestState, + SimpleObservable, + Symbols, + UpdateDelayedEventAction, } from "../src"; import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction"; import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction"; @@ -52,2484 +55,2659 @@ import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/R jest.mock("../src/transport/PostmessageTransport"); afterEach(() => { - jest.resetAllMocks(); + jest.resetAllMocks(); }); function createRoomEvent(event: Partial = {}): IRoomEvent { - return { - type: "m.room.message", - sender: "user-id", - content: {}, - origin_server_ts: 0, - event_id: "id-0", - room_id: "!room-id", - unsigned: {}, - ...event, - }; + return { + type: "m.room.message", + sender: "user-id", + content: {}, + origin_server_ts: 0, + event_id: "id-0", + room_id: "!room-id", + unsigned: {}, + ...event, + }; } class CustomMatrixError extends Error { - public constructor( - message: string, - public readonly httpStatus: number, - public readonly name: string, - public readonly data: Record, - ) { - super(message); - } + public constructor( + message: string, + public readonly httpStatus: number, + public readonly name: string, + public readonly data: Record, + ) { + super(message); + } } -function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetails | undefined { - return e instanceof CustomMatrixError - ? { - matrix_api_error: { - http_status: e.httpStatus, - http_headers: {}, - url: "", - response: { - errcode: e.name, - error: e.message, - ...e.data, - }, - }, - } - : undefined; +function processCustomMatrixError( + e: unknown, +): IWidgetApiErrorResponseDataDetails | undefined { + return e instanceof CustomMatrixError + ? { + matrix_api_error: { + http_status: e.httpStatus, + http_headers: {}, + url: "", + response: { + errcode: e.name, + error: e.message, + ...e.data, + }, + }, + } + : undefined; } describe("ClientWidgetApi", () => { - let capabilities: Capability[]; - let iframe: HTMLIFrameElement; - let driver: jest.Mocked; - let clientWidgetApi: ClientWidgetApi; - let transport: PostmessageTransport; - let emitEvent: Parameters["1"]; - - async function loadIframe(caps: Capability[] = []): Promise { - capabilities = caps; - - const ready = new Promise((resolve) => { - clientWidgetApi.once("ready", resolve); - }); - - iframe.dispatchEvent(new Event("load")); - - await ready; - } - - beforeEach(() => { - capabilities = []; - iframe = document.createElement("iframe"); - document.body.appendChild(iframe); - - driver = { - navigate: jest.fn(), - readRoomTimeline: jest.fn(), - readRoomState: jest.fn(() => Promise.resolve([])), - readEventRelations: jest.fn(), - sendEvent: jest.fn(), - sendDelayedEvent: jest.fn(), - updateDelayedEvent: jest.fn(), - sendToDevice: jest.fn(), - askOpenID: jest.fn(), - readRoomAccountData: jest.fn(), - validateCapabilities: jest.fn(), - searchUserDirectory: jest.fn(), - getMediaConfig: jest.fn(), - uploadFile: jest.fn(), - downloadFile: jest.fn(), - getKnownRooms: jest.fn(() => []), - processError: jest.fn(), - } as Partial as jest.Mocked; - - clientWidgetApi = new ClientWidgetApi( - new Widget({ - id: "test", - creatorUserId: "@alice:example.org", - type: "example", - url: "https://example.org", - }), - iframe, - driver, - ); - - [transport] = jest.mocked(PostmessageTransport).mock.instances; - emitEvent = jest.mocked(transport.on).mock.calls[0][1]; - - jest.mocked(transport.send).mockResolvedValue({}); - jest.mocked(driver.validateCapabilities).mockImplementation(async () => new Set(capabilities)); + let capabilities: Capability[]; + let iframe: HTMLIFrameElement; + let driver: jest.Mocked; + let clientWidgetApi: ClientWidgetApi; + let transport: PostmessageTransport; + let emitEvent: Parameters["1"]; + + async function loadIframe(caps: Capability[] = []): Promise { + capabilities = caps; + + const ready = new Promise((resolve) => { + clientWidgetApi.once("ready", resolve); }); - afterEach(() => { - clientWidgetApi.stop(); - iframe.remove(); + iframe.dispatchEvent(new Event("load")); + + await ready; + } + + beforeEach(() => { + capabilities = []; + iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + + driver = { + navigate: jest.fn(), + readRoomTimeline: jest.fn(), + readRoomState: jest.fn(() => Promise.resolve([])), + readEventRelations: jest.fn(), + sendEvent: jest.fn(), + sendDelayedEvent: jest.fn(), + updateDelayedEvent: jest.fn(), + sendToDevice: jest.fn(), + askOpenID: jest.fn(), + readRoomAccountData: jest.fn(), + validateCapabilities: jest.fn(), + searchUserDirectory: jest.fn(), + getMediaConfig: jest.fn(), + uploadFile: jest.fn(), + downloadFile: jest.fn(), + getKnownRooms: jest.fn(() => []), + processError: jest.fn(), + } as Partial as jest.Mocked; + + clientWidgetApi = new ClientWidgetApi( + new Widget({ + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org", + }), + iframe, + driver, + ); + + [transport] = jest.mocked(PostmessageTransport).mock.instances; + emitEvent = jest.mocked(transport.on).mock.calls[0][1]; + + jest.mocked(transport.send).mockResolvedValue({}); + jest + .mocked(driver.validateCapabilities) + .mockImplementation(async () => new Set(capabilities)); + }); + + afterEach(() => { + clientWidgetApi.stop(); + iframe.remove(); + }); + + it("should initiate capabilities", async () => { + await loadIframe(["m.always_on_screen"]); + + expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true); + expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false); + }); + + describe("navigate action", () => { + it("navigates", async () => { + driver.navigate.mockResolvedValue(Promise.resolve()); + + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + }; + + await loadIframe(["org.matrix.msc2931.navigate"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); + + expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); }); - it("should initiate capabilities", async () => { - await loadIframe(["m.always_on_screen"]); + it("fails to navigate", async () => { + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + }; - expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true); - expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false); - }); + await loadIframe([]); // Without the required capability - describe("navigate action", () => { - it("navigates", async () => { - driver.navigate.mockResolvedValue(Promise.resolve()); + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }); + }); - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; + expect(driver.navigate).not.toBeCalled(); + }); - await loadIframe(["org.matrix.msc2931.navigate"]); + it("fails to navigate to an unsupported URI", async () => { + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://example.net", + }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(["org.matrix.msc2931.navigate"]); - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); + emitEvent(new CustomEvent("", { detail: event })); - expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid matrix.to URI" }, }); + }); - it("fails to navigate", async () => { - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; + expect(driver.navigate).not.toBeCalled(); + }); - await loadIframe([]); // Without the required capability + it("should reject requests when the driver throws an exception", async () => { + driver.navigate.mockRejectedValue(new Error("M_UNKNOWN: Unknown error")); - emitEvent(new CustomEvent("", { detail: event })); + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + }; - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - }); + await loadIframe(["org.matrix.msc2931.navigate"]); - expect(driver.navigate).not.toBeCalled(); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("fails to navigate to an unsupported URI", async () => { - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://example.net", - }, - }; + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error handling navigation" }, + }); + }); + }); - await loadIframe(["org.matrix.msc2931.navigate"]); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.navigate.mockRejectedValue( + new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { + reason: "Unknown error", + }), + ); + + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + }; + + await loadIframe(["org.matrix.msc2931.navigate"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error handling navigation", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "failed to navigate", + reason: "Unknown error", + }, + } satisfies IMatrixApiError, + }, + }); + }); + }); + }); + + describe("send_event action", () => { + it("sends message events", async () => { + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; + + driver.sendEvent.mockResolvedValue({ + roomId, + eventId, + }); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + room_id: roomId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + event_id: eventId, + }); + }); + + expect(driver.sendEvent).toHaveBeenCalledWith( + event.data.type, + event.data.content, + null, + roomId, + ); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("sends state events", async () => { + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; + + driver.sendEvent.mockResolvedValue({ + roomId, + eventId, + }); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.topic", + content: {}, + state_key: "", + room_id: roomId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.state_event:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + event_id: eventId, + }); + }); + + expect(driver.sendEvent).toHaveBeenCalledWith( + event.data.type, + event.data.content, + "", + roomId, + ); + }); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid matrix.to URI" }, - }); - }); + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org"; + + driver.sendEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, + }); + }); + }); - expect(driver.navigate).not.toBeCalled(); + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org"; + + driver.processError.mockImplementation(processCustomMatrixError); + + driver.sendEvent.mockRejectedValue( + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", + }), + ); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); + + describe("send_event action for delayed events", () => { + it("fails to send delayed events", async () => { + const roomId = "!room:example.org"; + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + delay: 5000, + room_id: roomId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + // Without the required capability + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }); + }); + + expect(driver.sendDelayedEvent).not.toBeCalled(); + }); - it("should reject requests when the driver throws an exception", async () => { - driver.navigate.mockRejectedValue(new Error("M_UNKNOWN: Unknown error")); - - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; + it("sends delayed message events", async () => { + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + room_id: roomId, + delay: 5000, + parent_delay_id: parentDelayId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }); + }); + + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay, + event.data.parent_delay_id, + event.data.type, + event.data.content, + null, + roomId, + ); + }); - await loadIframe(["org.matrix.msc2931.navigate"]); + it("sends delayed state events", async () => { + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.topic", + content: {}, + state_key: "", + room_id: roomId, + delay: 5000, + parent_delay_id: parentDelayId, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.state_event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }); + }); + + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay, + event.data.parent_delay_id, + event.data.type, + event.data.content, + "", + roomId, + ); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org"; + + driver.sendDelayedEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + delay: 5000, + parent_delay_id: "fp", + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, + }); + }); + }); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error handling navigation" }, - }); - }); + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org"; + + driver.processError.mockImplementation(processCustomMatrixError); + + driver.sendDelayedEvent.mockRejectedValue( + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", + }), + ); + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + delay: 5000, + parent_delay_id: "fp", + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); + + describe("receiving events", () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + const event = createRoomEvent({ + room_id: roomId, + type: "m.room.message", + content: "hello", + }); + const eventFromOtherRoom = createRoomEvent({ + room_id: otherRoomId, + type: "m.room.message", + content: "test", + }); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.navigate.mockRejectedValue( - new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { - reason: "Unknown error", - }), - ); - - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; - - await loadIframe(["org.matrix.msc2931.navigate"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error handling navigation", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "failed to navigate", - reason: "Unknown error", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); + it("forwards events to the widget from one room only", async () => { + // Give the widget capabilities to receive from just one room + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]); + + // Event from the matching room should be forwarded + clientWidgetApi.feedEvent(event); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ); + + // Event from the other room should not be forwarded + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ); }); - describe("send_event action", () => { - it("sends message events", async () => { - const roomId = "!room:example.org"; - const eventId = "$event:example.org"; - - driver.sendEvent.mockResolvedValue({ - roomId, - eventId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - event_id: eventId, - }); - }); - - expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, null, roomId); - }); + it("forwards events to the widget from the currently viewed room", async () => { + clientWidgetApi.setViewedRoomId(roomId); + // Give the widget capabilities to receive events without specifying + // any rooms that it can read + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]); + + // Event from the viewed room should be forwarded + clientWidgetApi.feedEvent(event); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ); + + // Event from the other room should not be forwarded + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ); + + // View the other room; now the event can be forwarded + clientWidgetApi.setViewedRoomId(otherRoomId); + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ); + }); - it("sends state events", async () => { - const roomId = "!room:example.org"; - const eventId = "$event:example.org"; - - driver.sendEvent.mockResolvedValue({ - roomId, - eventId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.topic", - content: {}, - state_key: "", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.state_event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - event_id: eventId, - }); - }); - - expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, "", roomId); - }); + it("forwards events to the widget from all rooms", async () => { + // Give the widget capabilities to receive from any known room + await loadIframe([ + `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]); + + // Events from both rooms should be forwarded + clientWidgetApi.feedEvent(event); + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ); + }); + }); + + describe("receiving room state", () => { + it("syncs initial state and feeds updates", async () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + clientWidgetApi.setViewedRoomId(roomId); + const topicEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.topic", + state_key: "", + content: { topic: "Hello world!" }, + }); + const nameEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.name", + state_key: "", + content: { name: "Test room" }, + }); + const joinRulesEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "public" }, + }); + const otherRoomNameEvent = createRoomEvent({ + room_id: otherRoomId, + type: "m.room.name", + state_key: "", + content: { name: "Other room" }, + }); + + // Artificially delay the delivery of the join rules event + let resolveJoinRules: () => void; + const joinRules = new Promise( + (resolve) => (resolveJoinRules = resolve), + ); + + driver.readRoomState.mockImplementation( + async (rId, eventType, stateKey) => { + if (rId === roomId) { + if (eventType === "m.room.topic" && stateKey === "") + return [topicEvent]; + if (eventType === "m.room.name" && stateKey === "") + return [nameEvent]; + if (eventType === "m.room.join_rules" && stateKey === "") { + await joinRules; + return [joinRulesEvent]; + } + } else if (rId === otherRoomId) { + if (eventType === "m.room.name" && stateKey === "") + return [otherRoomNameEvent]; + } + return []; + }, + ); + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:m.room.topic#", + "org.matrix.msc2762.receive.state_event:m.room.name#", + "org.matrix.msc2762.receive.state_event:m.room.join_rules#", + ]); + + // Simulate a race between reading the original join rules event and + // the join rules being updated at the same time + const newJoinRulesEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "invite" }, + }); + clientWidgetApi.feedStateUpdate(newJoinRulesEvent); + // What happens if the original join rules are delivered after the + // updated ones? + resolveJoinRules!(); + + await waitFor(() => { + // The initial topic and name should have been pushed + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: [topicEvent, nameEvent, newJoinRulesEvent], + }, + ); + // Only the updated join rules should have been delivered + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([joinRules]), + }, + ); + }); + + // Check that further updates to room state are pushed to the widget + // as expected + const newTopicEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.topic", + state_key: "", + content: { topic: "Our new topic" }, + }); + clientWidgetApi.feedStateUpdate(newTopicEvent); + + await waitFor(() => { + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: [newTopicEvent], + }, + ); + }); + + // Up to this point we should not have received any state for the + // other (unviewed) room + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([otherRoomNameEvent]), + }, + ); + // Now view the other room + clientWidgetApi.setViewedRoomId(otherRoomId); + (transport.send as unknown as jest.SpyInstance).mockClear(); + + await waitFor(() => { + // The state of the other room should now be pushed + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([otherRoomNameEvent]), + }, + ); + }); + }); + }); - it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org"; - - driver.sendEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); - }); + describe("update_delayed_event action", () => { + it("fails to update delayed events", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + }; + + await loadIframe([]); // Without the required capability - it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org"; - - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendEvent.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { - reason: "Content must be a JSON object.", - }), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to send event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, }); + }); + + expect(driver.updateDelayedEvent).not.toBeCalled(); }); - describe("send_event action for delayed events", () => { - it("fails to send delayed events", async () => { - const roomId = "!room:example.org"; - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - delay: 5000, - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - // Without the required capability - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.sendDelayedEvent).not.toBeCalled(); - }); + it("fails to update delayed events with unsupported action", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: "unknown" as UpdateDelayedEventAction, + }, + }; - it("sends delayed message events", async () => { - const roomId = "!room:example.org"; - const parentDelayId = "fp"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, - }); - }); - - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, - event.data.type, - event.data.content, - null, - roomId, - ); - }); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - it("sends delayed state events", async () => { - const roomId = "!room:example.org"; - const parentDelayId = "fp"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.topic", - content: {}, - state_key: "", - room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.state_event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, - }); - }); - - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, - event.data.type, - event.data.content, - "", - roomId, - ); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org"; - - driver.sendDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - delay: 5000, - parent_delay_id: "fp", - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, }); + }); - it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org"; - - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendDelayedEvent.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { - reason: "Content must be a JSON object.", - }), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - delay: 5000, - parent_delay_id: "fp", - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to send event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); + expect(driver.updateDelayedEvent).not.toBeCalled(); }); - describe("receiving events", () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - const event = createRoomEvent({ room_id: roomId, type: "m.room.message", content: "hello" }); - const eventFromOtherRoom = createRoomEvent({ - room_id: otherRoomId, - type: "m.room.message", - content: "test", - }); - - it("forwards events to the widget from one room only", async () => { - // Give the widget capabilities to receive from just one room - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); + it("updates delayed events", async () => { + driver.updateDelayedEvent.mockResolvedValue(undefined); - // Event from the matching room should be forwarded - clientWidgetApi.feedEvent(event); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); + for (const action of [ + UpdateDelayedEventAction.Cancel, + UpdateDelayedEventAction.Restart, + UpdateDelayedEventAction.Send, + ]) { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action, + }, + }; - // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); - }); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - it("forwards events to the widget from the currently viewed room", async () => { - clientWidgetApi.setViewedRoomId(roomId); - // Give the widget capabilities to receive events without specifying - // any rooms that it can read - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); - - // Event from the viewed room should be forwarded - clientWidgetApi.feedEvent(event); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); - - // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); - - // View the other room; now the event can be forwarded - clientWidgetApi.setViewedRoomId(otherRoomId); - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("forwards events to the widget from all rooms", async () => { - // Give the widget capabilities to receive from any known room - await loadIframe([ - `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); - - // Events from both rooms should be forwarded - clientWidgetApi.feedEvent(event); - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); }); - }); - describe("receiving room state", () => { - it("syncs initial state and feeds updates", async () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - clientWidgetApi.setViewedRoomId(roomId); - const topicEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.topic", - state_key: "", - content: { topic: "Hello world!" }, - }); - const nameEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.name", - state_key: "", - content: { name: "Test room" }, - }); - const joinRulesEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.join_rules", - state_key: "", - content: { join_rule: "public" }, - }); - const otherRoomNameEvent = createRoomEvent({ - room_id: otherRoomId, - type: "m.room.name", - state_key: "", - content: { name: "Other room" }, - }); - - // Artificially delay the delivery of the join rules event - let resolveJoinRules: () => void; - const joinRules = new Promise((resolve) => (resolveJoinRules = resolve)); - - driver.readRoomState.mockImplementation(async (rId, eventType, stateKey) => { - if (rId === roomId) { - if (eventType === "m.room.topic" && stateKey === "") return [topicEvent]; - if (eventType === "m.room.name" && stateKey === "") return [nameEvent]; - if (eventType === "m.room.join_rules" && stateKey === "") { - await joinRules; - return [joinRulesEvent]; - } - } else if (rId === otherRoomId) { - if (eventType === "m.room.name" && stateKey === "") return [otherRoomNameEvent]; - } - return []; - }); - - await loadIframe([ - "org.matrix.msc2762.receive.state_event:m.room.topic#", - "org.matrix.msc2762.receive.state_event:m.room.name#", - "org.matrix.msc2762.receive.state_event:m.room.join_rules#", - ]); - - // Simulate a race between reading the original join rules event and - // the join rules being updated at the same time - const newJoinRulesEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.join_rules", - state_key: "", - content: { join_rule: "invite" }, - }); - clientWidgetApi.feedStateUpdate(newJoinRulesEvent); - // What happens if the original join rules are delivered after the - // updated ones? - resolveJoinRules!(); - - await waitFor(() => { - // The initial topic and name should have been pushed - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { - state: [topicEvent, nameEvent, newJoinRulesEvent], - }); - // Only the updated join rules should have been delivered - expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { - state: expect.arrayContaining([joinRules]), - }); - }); - - // Check that further updates to room state are pushed to the widget - // as expected - const newTopicEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.topic", - state_key: "", - content: { topic: "Our new topic" }, - }); - clientWidgetApi.feedStateUpdate(newTopicEvent); - - await waitFor(() => { - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { - state: [newTopicEvent], - }); - }); - - // Up to this point we should not have received any state for the - // other (unviewed) room - expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { - state: expect.arrayContaining([otherRoomNameEvent]), - }); - // Now view the other room - clientWidgetApi.setViewedRoomId(otherRoomId); - (transport.send as unknown as jest.SpyInstance).mockClear(); - - await waitFor(() => { - // The state of the other room should now be pushed - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { - state: expect.arrayContaining([otherRoomNameEvent]), - }); - }); - }); + expect(driver.updateDelayedEvent).toHaveBeenCalledWith( + event.data.delay_id, + event.data.action, + ); + } }); - describe("update_delayed_event action", () => { - it("fails to update delayed events", async () => { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; + it("should reject requests when the driver throws an exception", async () => { + driver.updateDelayedEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ); - await loadIframe([]); // Without the required capability + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - expect(driver.updateDelayedEvent).not.toBeCalled(); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error updating delayed event" }, }); + }); + }); - it("fails to update delayed events with unsupported action", async () => { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: "unknown" as UpdateDelayedEventAction, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.updateDelayedEvent).not.toBeCalled(); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.updateDelayedEvent.mockRejectedValue( + new CustomMatrixError( + "failed to update delayed event", + 400, + "M_NOT_JSON", + { + reason: "Content must be a JSON object.", + }, + ), + ); + + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + }; + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error updating delayed event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to update delayed event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); + + describe("send_to_device action", () => { + it("sends unencrypted to-device events", async () => { + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); + + expect(driver.sendToDevice).toHaveBeenCalledWith( + event.data.type, + event.data.encrypted, + event.data.messages, + ); + }); - it("updates delayed events", async () => { - driver.updateDelayedEvent.mockResolvedValue(undefined); - - for (const action of [ - UpdateDelayedEventAction.Cancel, - UpdateDelayedEventAction.Restart, - UpdateDelayedEventAction.Send, - ]) { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); - - expect(driver.updateDelayedEvent).toHaveBeenCalledWith(event.data.delay_id, event.data.action); - } - }); + it("fails to send to-device events without event type", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; - it("should reject requests when the driver throws an exception", async () => { - driver.updateDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); - - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error updating delayed event" }, - }); - }); - }); + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.updateDelayedEvent.mockRejectedValue( - new CustomMatrixError("failed to update delayed event", 400, "M_NOT_JSON", { - reason: "Content must be a JSON object.", - }), - ); - - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error updating delayed event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to update delayed event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - describe("send_to_device action", () => { - it("sends unencrypted to-device events", async () => { - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); - - expect(driver.sendToDevice).toHaveBeenCalledWith( - event.data.type, - event.data.encrypted, - event.data.messages, - ); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing event type" }, }); + }); - it("fails to send to-device events without event type", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event type" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); + expect(driver.sendToDevice).not.toBeCalled(); + }); - it("fails to send to-device events without event contents", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event contents" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); + it("fails to send to-device events without event contents", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + }, + }; - it("fails to send to-device events without encryption flag", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing encryption flag" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); - it("fails to send to-device events with any event type", async () => { - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}_different`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Cannot send to-device events of this type" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("should reject requests when the driver throws an exception", async () => { - driver.sendToDevice.mockRejectedValue( - new Error("M_FORBIDDEN: You don't have permission to send to-device events"), - ); - - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing event contents" }, }); + }); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendToDevice.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_FORBIDDEN", { - reason: "You don't have permission to send to-device events", - }), - ); - - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_FORBIDDEN", - error: "failed to send event", - reason: "You don't have permission to send to-device events", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); + expect(driver.sendToDevice).not.toBeCalled(); }); - describe("get_openid action", () => { - it("gets info", async () => { - driver.askOpenID.mockImplementation((observable) => { - observable.update({ - state: OpenIDRequestState.Allowed, - token: { - access_token: "access_token", - }, - }); - }); - - const event: IGetOpenIDActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.GetOpenIDCredentials, - data: {}, - }; - - await loadIframe([]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - state: OpenIDRequestState.Allowed, - access_token: "access_token", - }); - }); - - expect(driver.askOpenID).toHaveBeenCalledWith(expect.any(SimpleObservable)); - }); + it("fails to send to-device events without encryption flag", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; - it("fails when client provided invalid token", async () => { - driver.askOpenID.mockImplementation((observable) => { - observable.update({ - state: OpenIDRequestState.Allowed, - }); - }); + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); - const event: IGetOpenIDActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.GetOpenIDCredentials, - data: {}, - }; + emitEvent(new CustomEvent("", { detail: event })); - await loadIframe([]); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing encryption flag" }, + }); + }); + + expect(driver.sendToDevice).not.toBeCalled(); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("fails to send to-device events with any event type", async () => { + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - error: { message: "client provided invalid OIDC token for an allowed request" }, - }); - }); + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}_different`, + ]); - expect(driver.askOpenID).toHaveBeenCalledWith(expect.any(SimpleObservable)); - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - describe("com.beeper.read_room_account_data action", () => { - it("reads room account data", async () => { - const type = "net.example.test"; - const roomId = "!room:example.org"; - - driver.readRoomAccountData.mockResolvedValue([ - { - type, - room_id: roomId, - content: {}, - }, - ]); - - const event: IReadRoomAccountDataFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, - data: { - room_ids: [roomId], - type, - }, - }; - - await loadIframe([`com.beeper.capabilities.receive.room_account_data:${type}`]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - events: [ - { - type, - room_id: roomId, - content: {}, - }, - ], - }); - }); - - expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Cannot send to-device events of this type" }, }); + }); - it("does not read room account data", async () => { - const type = "net.example.test"; - const roomId = "!room:example.org"; - - driver.readRoomAccountData.mockResolvedValue([ - { - type, - room_id: roomId, - content: {}, - }, - ]); - - const event: IReadRoomAccountDataFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, - data: { - room_ids: [roomId], - type, - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - error: { message: "Cannot read room account data of this type" }, - }); - }); - - expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); - }); + expect(driver.sendToDevice).not.toBeCalled(); }); - describe("org.matrix.msc2876.read_events action", () => { - it("reads events from a specific room", async () => { - const roomId = "!room:example.org"; - const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test" }); - driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event]; - return []; - }); - - const request: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - room_ids: [roomId], - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:net.example.test", - ]); - clientWidgetApi.setViewedRoomId(roomId); - - emitEvent(new CustomEvent("", { detail: request })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(request, { - events: [event], - }); - }); - - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); + it("should reject requests when the driver throws an exception", async () => { + driver.sendToDevice.mockRejectedValue( + new Error( + "M_FORBIDDEN: You don't have permission to send to-device events", + ), + ); + + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; - it("reads events from all rooms", async () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test" }); - const otherRoomEvent = createRoomEvent({ room_id: otherRoomId, type: "net.example.test", content: "hi" }); - driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]); - driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event]; - if (rId === otherRoomId) return [otherRoomEvent]; - return []; - }); - - const request: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - room_ids: Symbols.AnyRoom, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - "org.matrix.msc2762.receive.event:net.example.test", - ]); - clientWidgetApi.setViewedRoomId(roomId); - - emitEvent(new CustomEvent("", { detail: request })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(request, { - events: [event, otherRoomEvent], - }); - }); - - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - otherRoomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); - it("reads state events with any state key", async () => { - driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ]); - - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: true, - }, - }; - - await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test"]); - clientWidgetApi.setViewedRoomId("!room-id"); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - events: [ - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ], - }); - }); - - expect(driver.readRoomTimeline).toBeCalledWith( - "!room-id", - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("fails to read state events with any state key", async () => { - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: true, - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.readRoomTimeline).not.toBeCalled(); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, }); + }); + }); - it("reads state events with a specific state key", async () => { - driver.readRoomTimeline.mockResolvedValue([createRoomEvent({ type: "net.example.test", state_key: "B" })]); - - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: "B", - }, - }; - - await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#B"]); - clientWidgetApi.setViewedRoomId("!room-id"); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - events: [createRoomEvent({ type: "net.example.test", state_key: "B" })], - }); - }); - - expect(driver.readRoomTimeline).toBeCalledWith( - "!room-id", - "net.example.test", - undefined, - "B", - 0, - undefined, - ); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.sendToDevice.mockRejectedValue( + new CustomMatrixError("failed to send event", 400, "M_FORBIDDEN", { + reason: "You don't have permission to send to-device events", + }), + ); + + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + }; + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_FORBIDDEN", + error: "failed to send event", + reason: "You don't have permission to send to-device events", + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); - it("fails to read state events with a specific state key", async () => { - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: "B", - }, - }; - - // Request the capability for the wrong state key - await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#A"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.readRoomTimeline).not.toBeCalled(); + describe("get_openid action", () => { + it("gets info", async () => { + driver.askOpenID.mockImplementation((observable) => { + observable.update({ + state: OpenIDRequestState.Allowed, + token: { + access_token: "access_token", + }, }); - }); + }); - describe("org.matrix.msc3869.read_relations action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; + const event: IGetOpenIDActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.GetOpenIDCredentials, + data: {}, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe([]); - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3869]), - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("should handle and process the request", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [createRoomEvent()], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe(["org.matrix.msc2762.receive.event:m.room.message"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [createRoomEvent()], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + state: OpenIDRequestState.Allowed, + access_token: "access_token", }); + }); - it("should only return events that match requested capabilities", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [ - createRoomEvent(), - createRoomEvent({ type: "m.reaction" }), - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe([ - "org.matrix.msc2762.receive.event:m.room.message", - "org.matrix.msc2762.receive.state_event:net.example.test#A", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [createRoomEvent(), createRoomEvent({ type: "net.example.test", state_key: "A" })], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); - }); + expect(driver.askOpenID).toHaveBeenCalledWith( + expect.any(SimpleObservable), + ); + }); - it("should accept all options and pass it to the driver", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!room-id", - event_type: "m.room.message", - rel_type: "m.reference", - limit: 25, - from: "from-token", - to: "to-token", - direction: "f", - }, - }; - - await loadIframe(["org.matrix.msc2762.timeline:!room-id"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - "!room-id", - "m.reference", - "m.room.message", - "from-token", - "to-token", - 25, - "f", - ); + it("fails when client provided invalid token", async () => { + driver.askOpenID.mockImplementation((observable) => { + observable.update({ + state: OpenIDRequestState.Allowed, }); + }); - it("should reject requests without event_id", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: {}, - }; + const event: IGetOpenIDActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.GetOpenIDCredentials, + data: {}, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe([]); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event ID" }, - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("should reject requests with a negative limit", async () => { - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - limit: -1, - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - limit out of range" }, - }); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + error: { + message: + "client provided invalid OIDC token for an allowed request", + }, }); + }); - it("should reject requests when the room timeline was not requested", async () => { - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!another-room-id", - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unable to access room timeline: !another-room-id" }, - }); - }); + expect(driver.askOpenID).toHaveBeenCalledWith( + expect.any(SimpleObservable), + ); + }); + }); + + describe("com.beeper.read_room_account_data action", () => { + it("reads room account data", async () => { + const type = "net.example.test"; + const roomId = "!room:example.org"; + + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]); + + const event: IReadRoomAccountDataFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, + data: { + room_ids: [roomId], + type, + }, + }; + + await loadIframe([ + `com.beeper.capabilities.receive.room_account_data:${type}`, + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + events: [ + { + type, + room_id: roomId, + content: {}, + }, + ], + }); + }); + + expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); + }); - it("should reject requests when the driver throws an exception", async () => { - driver.readEventRelations.mockRejectedValue( - new Error("M_FORBIDDEN: You don't have permission to access that event"), - ); + it("does not read room account data", async () => { + const type = "net.example.test"; + const roomId = "!room:example.org"; - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]); - await loadIframe(); + const event: IReadRoomAccountDataFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, + data: { + room_ids: [roomId], + type, + }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe([]); // Without the required capability - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while reading relations" }, - }); - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.readEventRelations.mockRejectedValue( - new CustomMatrixError("failed to read relations", 403, "M_FORBIDDEN", { - reason: "You don't have permission to access that event", - }), - ); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe(); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while reading relations", - matrix_api_error: { - http_status: 403, - http_headers: {}, - url: "", - response: { - errcode: "M_FORBIDDEN", - error: "failed to read relations", - reason: "You don't have permission to access that event", - }, - } satisfies IMatrixApiError, - }, - }); - }); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + error: { message: "Cannot read room account data of this type" }, }); + }); + + expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); + }); + }); + + describe("org.matrix.msc2876.read_events action", () => { + it("reads events from a specific room", async () => { + const roomId = "!room:example.org"; + const event = createRoomEvent({ + room_id: roomId, + type: "net.example.test", + content: "test", + }); + driver.readRoomTimeline.mockImplementation(async (rId) => { + if (rId === roomId) return [event]; + return []; + }); + + const request: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + room_ids: [roomId], + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:net.example.test", + ]); + clientWidgetApi.setViewedRoomId(roomId); + + emitEvent(new CustomEvent("", { detail: request })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(request, { + events: [event], + }); + }); + + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + roomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ); }); - describe("org.matrix.msc3973.user_directory_search action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; + it("reads events from all rooms", async () => { + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + const event = createRoomEvent({ + room_id: roomId, + type: "net.example.test", + content: "test", + }); + const otherRoomEvent = createRoomEvent({ + room_id: otherRoomId, + type: "net.example.test", + content: "hi", + }); + driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]); + driver.readRoomTimeline.mockImplementation(async (rId) => { + if (rId === roomId) return [event]; + if (rId === otherRoomId) return [otherRoomEvent]; + return []; + }); + + const request: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + room_ids: Symbols.AnyRoom, + }, + }; + + await loadIframe([ + `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, + "org.matrix.msc2762.receive.event:net.example.test", + ]); + clientWidgetApi.setViewedRoomId(roomId); + + emitEvent(new CustomEvent("", { detail: request })); + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(request, { + events: [event, otherRoomEvent], + }); + }); + + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + roomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ); + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + otherRoomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("reads state events with any state key", async () => { + driver.readRoomTimeline.mockResolvedValue([ + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ]); + + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: true, + }, + }; + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test", + ]); + clientWidgetApi.setViewedRoomId("!room-id"); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + events: [ + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ], + }); + }); + + expect(driver.readRoomTimeline).toBeCalledWith( + "!room-id", + "net.example.test", + undefined, + undefined, + 0, + undefined, + ); + }); - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3973]), - }); - }); + it("fails to read state events with any state key", async () => { + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: true, + }, + }; - it("should handle and process the request", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: true, - results: [ - { - userId: "@foo:bar.com", - }, - ], - }); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: true, - results: [ - { - user_id: "@foo:bar.com", - display_name: undefined, - avatar_url: undefined, - }, - ], - }); - }); - - expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); - }); + await loadIframe([]); // Without the required capability - it("should accept all options and pass it to the driver", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: false, - results: [ - { - userId: "@foo:bar.com", - }, - { - userId: "@bar:foo.com", - displayName: "Bar", - avatarUrl: "mxc://...", - }, - ], - }); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: 5, - }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: false, - results: [ - { - user_id: "@foo:bar.com", - display_name: undefined, - avatar_url: undefined, - }, - { - user_id: "@bar:foo.com", - display_name: "Bar", - avatar_url: "mxc://...", - }, - ], - }); - }); - - expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, }); + }); - it("should accept empty search_term", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: false, - results: [], - }); + expect(driver.readRoomTimeline).not.toBeCalled(); + }); - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "" }, - }; + it("reads state events with a specific state key", async () => { + driver.readRoomTimeline.mockResolvedValue([ + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ]); + + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: "B", + }, + }; + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test#B", + ]); + clientWidgetApi.setViewedRoomId("!room-id"); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + events: [ + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ], + }); + }); + + expect(driver.readRoomTimeline).toBeCalledWith( + "!room-id", + "net.example.test", + undefined, + "B", + 0, + undefined, + ); + }); - await loadIframe(["org.matrix.msc3973.user_directory_search"]); + it("fails to read state events with a specific state key", async () => { + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: "B", + }, + }; - emitEvent(new CustomEvent("", { detail: event })); + // Request the capability for the wrong state key + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test#A", + ]); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: false, - results: [], - }); - }); + emitEvent(new CustomEvent("", { detail: event })); - expect(driver.searchUserDirectory).toBeCalledWith("", undefined); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, }); + }); - it("should reject requests when the capability was not requested", async () => { - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; + expect(driver.readRoomTimeline).not.toBeCalled(); + }); + }); + + describe("org.matrix.msc3869.read_relations action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC3869, + ]), + }); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("should handle and process the request", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [createRoomEvent()], + }); + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + }; + + await loadIframe(["org.matrix.msc2762.receive.event:m.room.message"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [createRoomEvent()], + }); + }); + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + }); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); + it("should only return events that match requested capabilities", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [ + createRoomEvent(), + createRoomEvent({ type: "m.reaction" }), + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ], + }); + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + }; + + await loadIframe([ + "org.matrix.msc2762.receive.event:m.room.message", + "org.matrix.msc2762.receive.state_event:net.example.test#A", + ]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [ + createRoomEvent(), + createRoomEvent({ type: "net.example.test", state_key: "A" }), + ], + }); + }); + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + }); - expect(driver.searchUserDirectory).not.toBeCalled(); - }); + it("should accept all options and pass it to the driver", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [], + }); + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!room-id", + event_type: "m.room.message", + rel_type: "m.reference", + limit: 25, + from: "from-token", + to: "to-token", + direction: "f", + }, + }; + + await loadIframe(["org.matrix.msc2762.timeline:!room-id"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [], + }); + }); + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + "!room-id", + "m.reference", + "m.room.message", + "from-token", + "to-token", + 25, + "f", + ); + }); - it("should reject requests without search_term", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: {}, - }; + it("should reject requests without event_id", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: {}, + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]); + emitEvent(new CustomEvent("", { detail: event })); - emitEvent(new CustomEvent("", { detail: event })); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing event ID" }, + }); + }); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing search term" }, - }); + it("should reject requests with a negative limit", async () => { + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + limit: -1, + }, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - limit out of range" }, + }); + }); - expect(driver.searchUserDirectory).not.toBeCalled(); - }); + it("should reject requests when the room timeline was not requested", async () => { + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!another-room-id", + }, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Unable to access room timeline: !another-room-id" }, + }); + }); - it("should reject requests with a negative limit", async () => { - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: -1, - }, - }; + it("should reject requests when the driver throws an exception", async () => { + driver.readEventRelations.mockRejectedValue( + new Error( + "M_FORBIDDEN: You don't have permission to access that event", + ), + ); - await loadIframe(["org.matrix.msc3973.user_directory_search"]); + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - limit out of range" }, - }); + emitEvent(new CustomEvent("", { detail: event })); - expect(driver.searchUserDirectory).not.toBeCalled(); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Unexpected error while reading relations" }, }); + }); + }); - it("should reject requests when the driver throws an exception", async () => { - driver.searchUserDirectory.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.readEventRelations.mockRejectedValue( + new CustomMatrixError("failed to read relations", 403, "M_FORBIDDEN", { + reason: "You don't have permission to access that event", + }), + ); + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + }; + + await loadIframe(); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while reading relations", + matrix_api_error: { + http_status: 403, + http_headers: {}, + url: "", + response: { + errcode: "M_FORBIDDEN", + error: "failed to read relations", + reason: "You don't have permission to access that event", + }, + } satisfies IMatrixApiError, + }, + }); + }); + }); + }); + + describe("org.matrix.msc3973.user_directory_search action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC3973, + ]), + }); + }); - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; + it("should handle and process the request", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: true, + results: [ + { + userId: "@foo:bar.com", + }, + ], + }); + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + }; + + await loadIframe(["org.matrix.msc3973.user_directory_search"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: true, + results: [ + { + user_id: "@foo:bar.com", + display_name: undefined, + avatar_url: undefined, + }, + ], + }); + }); + + expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); + }); - await loadIframe(["org.matrix.msc3973.user_directory_search"]); + it("should accept all options and pass it to the driver", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: false, + results: [ + { + userId: "@foo:bar.com", + }, + { + userId: "@bar:foo.com", + displayName: "Bar", + avatarUrl: "mxc://...", + }, + ], + }); + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: 5, + }, + }; + + await loadIframe(["org.matrix.msc3973.user_directory_search"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: false, + results: [ + { + user_id: "@foo:bar.com", + display_name: undefined, + avatar_url: undefined, + }, + { + user_id: "@bar:foo.com", + display_name: "Bar", + avatar_url: "mxc://...", + }, + ], + }); + }); + + expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); + }); - emitEvent(new CustomEvent("", { detail: event })); + it("should accept empty search_term", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: false, + results: [], + }); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while searching in the user directory" }, - }); - }); - }); + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "" }, + }; - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.searchUserDirectory.mockRejectedValue( - new CustomMatrixError("failed to search the user directory", 429, "M_LIMIT_EXCEEDED", { - reason: "Too many requests", - retry_after_ms: 2000, - }), - ); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while searching in the user directory", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to search the user directory", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: false, + results: [], }); + }); + + expect(driver.searchUserDirectory).toBeCalledWith("", undefined); }); - describe("org.matrix.msc4039.get_media_config action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; + it("should reject requests when the capability was not requested", async () => { + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + }; - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), - }); - }); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }); - it("should handle and process the request", async () => { - driver.getMediaConfig.mockResolvedValue({ - "m.upload.size": 1000, - }); + expect(driver.searchUserDirectory).not.toBeCalled(); + }); - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; + it("should reject requests without search_term", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: {}, + }; - await loadIframe(["org.matrix.msc4039.upload_file"]); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - "m.upload.size": 1000, - }); - }); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing search term" }, + }); - expect(driver.getMediaConfig).toBeCalled(); - }); + expect(driver.searchUserDirectory).not.toBeCalled(); + }); - it("should reject requests when the capability was not requested", async () => { - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; + it("should reject requests with a negative limit", async () => { + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: -1, + }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); + emitEvent(new CustomEvent("", { detail: event })); - expect(driver.getMediaConfig).not.toBeCalled(); - }); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - limit out of range" }, + }); - it("should reject requests when the driver throws an exception", async () => { - driver.getMediaConfig.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); + expect(driver.searchUserDirectory).not.toBeCalled(); + }); - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; + it("should reject requests when the driver throws an exception", async () => { + driver.searchUserDirectory.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); - await loadIframe(["org.matrix.msc4039.upload_file"]); + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while getting the media configuration" }, - }); - }); + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while searching in the user directory", + }, }); + }); + }); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.getMediaConfig.mockRejectedValue( - new CustomMatrixError("failed to get the media configuration", 429, "M_LIMIT_EXCEEDED", { - reason: "Too many requests", - retry_after_ms: 2000, - }), - ); - - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while getting the media configuration", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to get the media configuration", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.searchUserDirectory.mockRejectedValue( + new CustomMatrixError( + "failed to search the user directory", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ); + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + }; + + await loadIframe(["org.matrix.msc3973.user_directory_search"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while searching in the user directory", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to search the user directory", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); + + describe("org.matrix.msc4039.get_media_config action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }); }); - describe("MSC4039", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; + it("should handle and process the request", async () => { + driver.getMediaConfig.mockResolvedValue({ + "m.upload.size": 1000, + }); + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; - emitEvent(new CustomEvent("", { detail: event })); + await loadIframe(["org.matrix.msc4039.upload_file"]); - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), - }); + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + "m.upload.size": 1000, }); + }); + + expect(driver.getMediaConfig).toBeCalled(); }); - describe("org.matrix.msc4039.upload_file action", () => { - it("should handle and process the request", async () => { - driver.uploadFile.mockResolvedValue({ - contentUri: "mxc://...", - }); + it("should reject requests when the capability was not requested", async () => { + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; + emitEvent(new CustomEvent("", { detail: event })); - await loadIframe(["org.matrix.msc4039.upload_file"]); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }); - emitEvent(new CustomEvent("", { detail: event })); + expect(driver.getMediaConfig).not.toBeCalled(); + }); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - content_uri: "mxc://...", - }); - }); + it("should reject requests when the driver throws an exception", async () => { + driver.getMediaConfig.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); - expect(driver.uploadFile).toBeCalled(); - }); + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; - it("should reject requests when the capability was not requested", async () => { - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while getting the media configuration", + }, + }); + }); + }); - expect(driver.uploadFile).not.toBeCalled(); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.getMediaConfig.mockRejectedValue( + new CustomMatrixError( + "failed to get the media configuration", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ); + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + }; + + await loadIframe(["org.matrix.msc4039.upload_file"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while getting the media configuration", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to get the media configuration", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); + + describe("MSC4039", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }); + }); + }); - it("should reject requests when the driver throws an exception", async () => { - driver.uploadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); + describe("org.matrix.msc4039.upload_file action", () => { + it("should handle and process the request", async () => { + driver.uploadFile.mockResolvedValue({ + contentUri: "mxc://...", + }); - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + }; - await loadIframe(["org.matrix.msc4039.upload_file"]); + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while uploading a file" }, - }); - }); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + content_uri: "mxc://...", }); + }); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.uploadFile.mockRejectedValue( - new CustomMatrixError("failed to upload a file", 429, "M_LIMIT_EXCEEDED", { - reason: "Too many requests", - retry_after_ms: 2000, - }), - ); - - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while uploading a file", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to upload a file", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); + expect(driver.uploadFile).toBeCalled(); }); - describe("org.matrix.msc4039.download_file action", () => { - it("should handle and process the request", async () => { - driver.downloadFile.mockResolvedValue({ - file: "test contents", - }); + it("should reject requests when the capability was not requested", async () => { + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + }; - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; + emitEvent(new CustomEvent("", { detail: event })); - await loadIframe(["org.matrix.msc4039.download_file"]); + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }); - emitEvent(new CustomEvent("", { detail: event })); + expect(driver.uploadFile).not.toBeCalled(); + }); - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - file: "test contents", - }); - }); + it("should reject requests when the driver throws an exception", async () => { + driver.uploadFile.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); - expect(driver.downloadFile).toHaveBeenCalledWith("mxc://example.com/test_file"); - }); + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + }; - it("should reject requests when the capability was not requested", async () => { - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Unexpected error while uploading a file" }, + }); + }); + }); - expect(driver.uploadFile).not.toBeCalled(); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.uploadFile.mockRejectedValue( + new CustomMatrixError( + "failed to upload a file", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ); + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + }; + + await loadIframe(["org.matrix.msc4039.upload_file"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while uploading a file", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to upload a file", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, }); + }); + }); + }); - it("should reject requests when the driver throws an exception", async () => { - driver.downloadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); + describe("org.matrix.msc4039.download_file action", () => { + it("should handle and process the request", async () => { + driver.downloadFile.mockResolvedValue({ + file: "test contents", + }); - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + }; - await loadIframe(["org.matrix.msc4039.download_file"]); + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent("", { detail: event })); + emitEvent(new CustomEvent("", { detail: event })); - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while downloading a file" }, - }); - }); + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + file: "test contents", }); + }); - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.downloadFile.mockRejectedValue( - new CustomMatrixError("failed to download a file", 429, "M_LIMIT_EXCEEDED", { - reason: "Too many requests", - retry_after_ms: 2000, - }), - ); - - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; - - await loadIframe(["org.matrix.msc4039.download_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while downloading a file", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to download a file", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); + expect(driver.downloadFile).toHaveBeenCalledWith( + "mxc://example.com/test_file", + ); }); - it("updates theme", () => { - clientWidgetApi.updateTheme({ name: "dark" }); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.ThemeChange, { name: "dark" }); + it("should reject requests when the capability was not requested", async () => { + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + }; + + emitEvent(new CustomEvent("", { detail: event })); + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }); + + expect(driver.uploadFile).not.toBeCalled(); + }); + + it("should reject requests when the driver throws an exception", async () => { + driver.downloadFile.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ); + + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + }; + + await loadIframe(["org.matrix.msc4039.download_file"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Unexpected error while downloading a file" }, + }); + }); }); - it("updates language", () => { - clientWidgetApi.updateLanguage("tlh"); - expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.LanguageChange, { lang: "tlh" }); + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError); + + driver.downloadFile.mockRejectedValue( + new CustomMatrixError( + "failed to download a file", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ); + + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + }; + + await loadIframe(["org.matrix.msc4039.download_file"]); + + emitEvent(new CustomEvent("", { detail: event })); + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while downloading a file", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to download a file", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, + }); + }); }); + }); + + it("updates theme", () => { + clientWidgetApi.updateTheme({ name: "dark" }); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.ThemeChange, + { name: "dark" }, + ); + }); + + it("updates language", () => { + clientWidgetApi.updateLanguage("tlh"); + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.LanguageChange, + { lang: "tlh" }, + ); + }); }); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index b128e1c..c3870ab 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -26,731 +26,831 @@ import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/Us import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction"; import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi"; import { - IWidgetApiErrorResponseData, - IWidgetApiErrorResponseDataDetails, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - UpdateDelayedEventAction, - WidgetApiDirection, + IWidgetApiErrorResponseData, + IWidgetApiErrorResponseDataDetails, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + UpdateDelayedEventAction, + WidgetApiDirection, } from "../src"; type SendRequestArgs = { - action: WidgetApiFromWidgetAction; - data: IWidgetApiRequestData; + action: WidgetApiFromWidgetAction; + data: IWidgetApiRequestData; }; class TransportChannels { - /** Data sent by widget requests */ - public readonly requestQueue: Array = []; - /** Responses to send as if from a client. Initialized with the response to {@link WidgetApi.start}*/ - public readonly responseQueue: IWidgetApiResponseData[] = [ - { supported_versions: [] } satisfies ISupportedVersionsActionResponseData, - ]; + /** Data sent by widget requests */ + public readonly requestQueue: Array = []; + /** Responses to send as if from a client. Initialized with the response to {@link WidgetApi.start}*/ + public readonly responseQueue: IWidgetApiResponseData[] = [ + { supported_versions: [] } satisfies ISupportedVersionsActionResponseData, + ]; } class WidgetTransportHelper { - /** For ignoring the request sent by {@link WidgetApi.start} */ - private skippedFirstRequest = false; + /** For ignoring the request sent by {@link WidgetApi.start} */ + private skippedFirstRequest = false; - public constructor(private channels: TransportChannels) {} + public constructor(private channels: TransportChannels) {} - public nextTrackedRequest(): SendRequestArgs | undefined { - if (!this.skippedFirstRequest) { - this.skippedFirstRequest = true; - this.channels.requestQueue.shift(); - } - return this.channels.requestQueue.shift(); + public nextTrackedRequest(): SendRequestArgs | undefined { + if (!this.skippedFirstRequest) { + this.skippedFirstRequest = true; + this.channels.requestQueue.shift(); } + return this.channels.requestQueue.shift(); + } - public queueResponse(data: IWidgetApiResponseData): void { - this.channels.responseQueue.push(data); - } + public queueResponse(data: IWidgetApiResponseData): void { + this.channels.responseQueue.push(data); + } } class ClientTransportHelper { - public constructor(private channels: TransportChannels) {} - - public trackRequest(action: WidgetApiFromWidgetAction, data: IWidgetApiRequestData): void { - this.channels.requestQueue.push({ action, data }); - } - - public nextQueuedResponse(): IWidgetApiRequestData | undefined { - return this.channels.responseQueue.shift(); - } + public constructor(private channels: TransportChannels) {} + + public trackRequest( + action: WidgetApiFromWidgetAction, + data: IWidgetApiRequestData, + ): void { + this.channels.requestQueue.push({ action, data }); + } + + public nextQueuedResponse(): IWidgetApiRequestData | undefined { + return this.channels.responseQueue.shift(); + } } describe("WidgetApi", () => { - let widgetApi: WidgetApi; - let widgetTransportHelper: WidgetTransportHelper; - let clientListener: (e: MessageEvent) => void; + let widgetApi: WidgetApi; + let widgetTransportHelper: WidgetTransportHelper; + let clientListener: (e: MessageEvent) => void; + + beforeEach(() => { + const channels = new TransportChannels(); + widgetTransportHelper = new WidgetTransportHelper(channels); + const clientTrafficHelper = new ClientTransportHelper(channels); + + clientListener = (e: MessageEvent): void => { + if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response + if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) + return; // not a request + const request = e.data; + + clientTrafficHelper.trackRequest( + request.action as WidgetApiFromWidgetAction, + request.data, + ); + + const response = clientTrafficHelper.nextQueuedResponse(); + if (response) { + window.postMessage( + { + ...request, + response: response, + } satisfies IWidgetApiResponse, + "*", + ); + } + }; + window.addEventListener("message", clientListener); + + widgetApi = new WidgetApi("WidgetApi-test", "*"); + widgetApi.start(); + }); + + afterEach(() => { + window.removeEventListener("message", clientListener); + }); + + describe("readEventRelations", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + chunk: [], + } as IReadRelationsFromWidgetResponseData); + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).resolves.toEqual({ + chunk: [], + }); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!room-id", + rel_type: "m.reference", + event_type: "m.room.message", + limit: 25, + from: "from-token", + to: "to-token", + direction: "f", + }, + } satisfies SendRequestArgs); + }); + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData); + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow( + "The read_relations action is not supported by the client.", + ); + + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: expect.anything(), + } satisfies SendRequestArgs); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow("An error occurred"); + }); + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData); + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("sendEvent", () => { + it("sends message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + event_id: "$event", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", + }); + }); + + it("sends state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + event_id: "$event", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id"), + ).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", + }); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).rejects.toThrow("An error occurred"); + }); + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("delayed sendEvent", () => { + it("sends delayed message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }); + }); + + it("sends delayed state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 2000), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }); + }); + + it("sends delayed child action message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }); + }); + + it("sends delayed child action state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData); + + await expect( + widgetApi.sendStateEvent( + "m.room.topic", + "", + {}, + "!room-id", + 1000, + undefined, + ), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).rejects.toThrow("An error occurred"); + }); + + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("updateDelayedEvent", () => { + it("updates delayed events", async () => { + widgetTransportHelper.queueResponse({}); + await expect( + widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), + ).resolves.toEqual({}); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), + ).rejects.toThrow("An error occurred"); + }); + + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("getClientVersions", () => { beforeEach(() => { - const channels = new TransportChannels(); - widgetTransportHelper = new WidgetTransportHelper(channels); - const clientTrafficHelper = new ClientTransportHelper(channels); - - clientListener = (e: MessageEvent): void => { - if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response - if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request - const request = e.data; - - clientTrafficHelper.trackRequest(request.action as WidgetApiFromWidgetAction, request.data); - - const response = clientTrafficHelper.nextQueuedResponse(); - if (response) { - window.postMessage( - { - ...request, - response: response, - } satisfies IWidgetApiResponse, - "*", - ); - } - }; - window.addEventListener("message", clientListener); - - widgetApi = new WidgetApi("WidgetApi-test", "*"); - widgetApi.start(); - }); - - afterEach(() => { - window.removeEventListener("message", clientListener); - }); - - describe("readEventRelations", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - chunk: [], - } as IReadRelationsFromWidgetResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).resolves.toEqual({ - chunk: [], - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!room-id", - rel_type: "m.reference", - event_type: "m.room.message", - limit: 25, - from: "from-token", - to: "to-token", - direction: "f", - }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow("The read_relations action is not supported by the client."); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow(new WidgetApiResponseError("An error occurred", errorDetails)); - }); - }); - - describe("sendEvent", () => { - it("sends message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - event_id: "$event", - } as ISendEventFromWidgetResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).resolves.toEqual({ - room_id: "!room-id", - event_id: "$event", - }); - }); - - it("sends state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - event_id: "$event", - } as ISendEventFromWidgetResponseData); - - await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id")).resolves.toEqual({ - room_id: "!room-id", - event_id: "$event", - }); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("delayed sendEvent", () => { - it("sends delayed message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000)).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 2000)).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed child action message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed child action state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 1000, undefined), - ).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("updateDelayedEvent", () => { - it("updates delayed events", async () => { - widgetTransportHelper.queueResponse({}); - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).resolves.toEqual({}); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("getClientVersions", () => { - beforeEach(() => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869, UnstableApiVersion.MSC2762], - } as ISupportedVersionsActionResponseData); - }); - - it("should request supported client versions", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); - }); - - it("should cache supported client versions on successive calls", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); - - await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); - }); - }); - - describe("searchUserDirectory", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - limited: false, - results: [], - } as IUserDirectorySearchFromWidgetResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).resolves.toEqual({ - limited: false, - results: [], - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: 10, - }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( - "The user_directory_search action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("getMediaConfig", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - "m.upload.size": 1000, - } as IGetMediaConfigActionFromWidgetResponseData); - - await expect(widgetApi.getMediaConfig()).resolves.toEqual({ - "m.upload.size": 1000, - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - "The get_media_config action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("uploadFile", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - content_uri: "mxc://...", - } as IUploadFileActionFromWidgetResponseData); - - await expect(widgetApi.uploadFile("data")).resolves.toEqual({ - content_uri: "mxc://...", - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { file: "data" }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - "The upload_file action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("downloadFile", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ file: "test contents" } as IDownloadFileActionFromWidgetResponseData); - - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).resolves.toEqual({ - file: "test contents", - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { content_uri: "mxc://example.com/test_file" }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ supported_versions: [] } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( - "The download_file action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ error: { message: "An error occurred" } }); - - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); + widgetTransportHelper.queueResponse({ + supported_versions: [ + UnstableApiVersion.MSC3869, + UnstableApiVersion.MSC2762, + ], + } as ISupportedVersionsActionResponseData); + }); + + it("should request supported client versions", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]); + }); + + it("should cache supported client versions on successive calls", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]); + + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); + }); + }); + + describe("searchUserDirectory", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + limited: false, + results: [], + } as IUserDirectorySearchFromWidgetResponseData); + + await expect(widgetApi.searchUserDirectory("foo", 10)).resolves.toEqual({ + limited: false, + results: [], + }); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: 10, + }, + } satisfies SendRequestArgs); + }); + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData); + + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( + "The user_directory_search action is not supported by the client.", + ); + + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: expect.anything(), + } satisfies SendRequestArgs); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }); + + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( + "An error occurred", + ); + }); + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData); + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("getMediaConfig", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + "m.upload.size": 1000, + } as IGetMediaConfigActionFromWidgetResponseData); + + await expect(widgetApi.getMediaConfig()).resolves.toEqual({ + "m.upload.size": 1000, + }); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } satisfies SendRequestArgs); + }); + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData); + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + "The get_media_config action is not supported by the client.", + ); + + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }); + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + "An error occurred", + ); + }); + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("uploadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + content_uri: "mxc://...", + } as IUploadFileActionFromWidgetResponseData); + + await expect(widgetApi.uploadFile("data")).resolves.toEqual({ + content_uri: "mxc://...", + }); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { file: "data" }, + } satisfies SendRequestArgs); + }); + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData); + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "The upload_file action is not supported by the client.", + ); + + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }); + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "An error occurred", + ); + }); + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); + }); + }); + + describe("downloadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + file: "test contents", + } as IDownloadFileActionFromWidgetResponseData); + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).resolves.toEqual({ + file: "test contents", + }); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { content_uri: "mxc://example.com/test_file" }, + } satisfies SendRequestArgs); + }); + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData); + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow( + "The download_file action is not supported by the client.", + ); + + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs); + }); + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }); + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow("An error occurred"); + }); + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData); + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + }; + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData); + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ); }); + }); }); diff --git a/test/url-template-test.ts b/test/url-template-test.ts index 3f28df8..ee67028 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -17,41 +17,47 @@ import { runTemplate } from "../src"; describe("runTemplate", () => { - it("should replace device id template in url", () => { - const url = "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; - const replacedUrl = runTemplate( - url, - { - id: "widget-id", - creatorUserId: "@user-id", - type: "type", - url, - }, - { - deviceId: "my-device-id", - currentUserId: "@user-id", - }, - ); + it("should replace device id template in url", () => { + const url = + "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; + const replacedUrl = runTemplate( + url, + { + id: "widget-id", + creatorUserId: "@user-id", + type: "type", + url, + }, + { + deviceId: "my-device-id", + currentUserId: "@user-id", + }, + ); - expect(replacedUrl).toBe("https://localhost/?my-query#device_id=my-device-id"); - }); + expect(replacedUrl).toBe( + "https://localhost/?my-query#device_id=my-device-id", + ); + }); - it("should replace base url template in url", () => { - const url = "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; - const replacedUrl = runTemplate( - url, - { - id: "widget-id", - creatorUserId: "@user-id", - type: "type", - url, - }, - { - currentUserId: "@user-id", - baseUrl: "https://localhost/api", - }, - ); + it("should replace base url template in url", () => { + const url = + "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; + const replacedUrl = runTemplate( + url, + { + id: "widget-id", + creatorUserId: "@user-id", + type: "type", + url, + }, + { + currentUserId: "@user-id", + baseUrl: "https://localhost/api", + }, + ); - expect(replacedUrl).toBe("https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi"); - }); + expect(replacedUrl).toBe( + "https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi", + ); + }); }); From ccff9cdb68bc6016874e6a26eb9fd2899f125b35 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:51:49 +0100 Subject: [PATCH 08/11] move prettier to overwrites --- .eslintrc.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d3eb235..45a1236 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,10 +1,6 @@ module.exports = { plugins: ["matrix-org"], - extends: [ - "plugin:matrix-org/babel", - "plugin:matrix-org/typescript", - "prettier", - ], + extends: ["plugin:matrix-org/babel"], parserOptions: { project: ["./tsconfig-dev.json"], }, @@ -38,7 +34,7 @@ module.exports = { overrides: [ { files: ["src/**/*.ts", "test/**/*.ts"], - extends: ["plugin:matrix-org/typescript"], + extends: ["plugin:matrix-org/typescript", "prettier"], rules: { // TypeScript has its own version of this "babel/no-invalid-this": "off", From 5b258236946f56eb3191bd8b9dc012c0a1b55998 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 18:06:33 +0100 Subject: [PATCH 09/11] back to 4 spaces --- .babelrc | 6 +- .eslintrc.js | 98 +- .github/workflows/build.yaml | 66 +- .github/workflows/release.yaml | 96 +- .github/workflows/sonarqube.yml | 24 +- .prettierrc | 4 + README.md | 60 +- examples/widget/index.css | 22 +- examples/widget/index.html | 155 +- examples/widget/utils.js | 22 +- jest.config.ts | 20 +- package.json | 152 +- renovate.json | 4 +- src/ClientWidgetApi.ts | 2817 ++++----- src/Symbols.ts | 2 +- src/WidgetApi.ts | 2037 ++++--- src/driver/WidgetDriver.ts | 748 +-- src/index.ts | 108 +- src/interfaces/ApiVersion.ts | 60 +- src/interfaces/Capabilities.ts | 92 +- src/interfaces/CapabilitiesAction.ts | 62 +- src/interfaces/ContentLoadedAction.ts | 18 +- src/interfaces/DownloadFileAction.ts | 24 +- src/interfaces/GetMediaConfigAction.ts | 22 +- src/interfaces/GetOpenIDAction.ts | 34 +- src/interfaces/ICustomWidgetData.ts | 10 +- src/interfaces/IJitsiWidgetData.ts | 28 +- src/interfaces/IRoomAccountData.ts | 6 +- src/interfaces/IRoomEvent.ts | 16 +- src/interfaces/IStickerpickerWidgetData.ts | 4 +- src/interfaces/IWidget.ts | 78 +- src/interfaces/IWidgetApiErrorResponse.ts | 58 +- src/interfaces/IWidgetApiRequest.ts | 26 +- src/interfaces/IWidgetApiResponse.ts | 10 +- src/interfaces/LanguageChangeAction.ts | 24 +- src/interfaces/ModalButtonKind.ts | 10 +- src/interfaces/ModalWidgetActions.ts | 72 +- src/interfaces/NavigateAction.ts | 14 +- src/interfaces/OpenIDCredentialsAction.ts | 28 +- src/interfaces/ReadEventAction.ts | 34 +- src/interfaces/ReadRelationsAction.ts | 44 +- src/interfaces/ReadRoomAccountDataAction.ts | 30 +- src/interfaces/ScreenshotAction.ts | 18 +- src/interfaces/SendEventAction.ts | 58 +- src/interfaces/SendToDeviceAction.ts | 52 +- src/interfaces/SetModalButtonEnabledAction.ts | 22 +- src/interfaces/StickerAction.ts | 48 +- src/interfaces/StickyAction.ts | 16 +- src/interfaces/SupportedVersionsAction.ts | 32 +- src/interfaces/ThemeChangeAction.ts | 14 +- src/interfaces/TurnServerActions.ts | 48 +- src/interfaces/UpdateDelayedEventAction.ts | 32 +- src/interfaces/UpdateStateAction.ts | 22 +- src/interfaces/UploadFileAction.ts | 24 +- src/interfaces/UserDirectorySearchAction.ts | 36 +- src/interfaces/VisibilityAction.ts | 14 +- src/interfaces/WidgetApiAction.ts | 134 +- src/interfaces/WidgetApiDirection.ts | 18 +- src/interfaces/WidgetConfigAction.ts | 18 +- src/interfaces/WidgetKind.ts | 6 +- src/interfaces/WidgetType.ts | 8 +- src/models/Widget.ts | 152 +- src/models/WidgetEventCapability.ts | 507 +- src/models/WidgetParser.ts | 248 +- src/models/validation/url.ts | 24 +- src/models/validation/utils.ts | 10 +- src/templating/url-template.ts | 93 +- src/transport/ITransport.ts | 160 +- src/transport/PostmessageTransport.ts | 357 +- src/util/SimpleObservable.ts | 30 +- test/ClientWidgetApi-test.ts | 5424 +++++++++-------- test/WidgetApi-test.ts | 1678 ++--- test/url-template-test.ts | 84 +- tsconfig-dev.json | 4 +- tsconfig.json | 30 +- 75 files changed, 8486 insertions(+), 8180 deletions(-) create mode 100644 .prettierrc diff --git a/.babelrc b/.babelrc index e0f4da4..199c68a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "sourceMaps": true, - "presets": ["@babel/preset-env", "@babel/preset-typescript"], - "plugins": ["@babel/plugin-proposal-class-properties"] + "sourceMaps": true, + "presets": ["@babel/preset-env", "@babel/preset-typescript"], + "plugins": ["@babel/plugin-proposal-class-properties"] } diff --git a/.eslintrc.js b/.eslintrc.js index 45a1236..57b1a39 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,52 +1,52 @@ module.exports = { - plugins: ["matrix-org"], - extends: ["plugin:matrix-org/babel"], - parserOptions: { - project: ["./tsconfig-dev.json"], - }, - env: { - browser: true, - }, - rules: { - "no-var": ["warn"], - "prefer-rest-params": ["warn"], - "prefer-spread": ["warn"], - "one-var": ["warn"], - "padded-blocks": ["warn"], - "no-extend-native": ["warn"], - camelcase: ["warn"], - "no-multi-spaces": ["error", { ignoreEOLComments: true }], - "space-before-function-paren": [ - "error", - { - anonymous: "never", - named: "never", - asyncArrow: "always", - }, - ], - "arrow-parens": "off", - "prefer-promise-reject-errors": "off", - quotes: "off", - indent: "off", - "no-constant-condition": "off", - "no-async-promise-executor": "off", - }, - overrides: [ - { - files: ["src/**/*.ts", "test/**/*.ts"], - extends: ["plugin:matrix-org/typescript", "prettier"], - rules: { - // TypeScript has its own version of this - "babel/no-invalid-this": "off", - - quotes: "off", - }, + plugins: ["matrix-org"], + extends: ["plugin:matrix-org/babel"], + parserOptions: { + project: ["./tsconfig-dev.json"], }, - { - files: ["src/interfaces/**/*.ts"], - rules: { - "@typescript-eslint/no-empty-object-type": "off", - }, + env: { + browser: true, }, - ], -}; + rules: { + "no-var": ["warn"], + "prefer-rest-params": ["warn"], + "prefer-spread": ["warn"], + "one-var": ["warn"], + "padded-blocks": ["warn"], + "no-extend-native": ["warn"], + camelcase: ["warn"], + "no-multi-spaces": ["error", { ignoreEOLComments: true }], + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always", + }, + ], + "arrow-parens": "off", + "prefer-promise-reject-errors": "off", + quotes: "off", + indent: "off", + "no-constant-condition": "off", + "no-async-promise-executor": "off", + }, + overrides: [ + { + files: ["src/**/*.ts", "test/**/*.ts"], + extends: ["plugin:matrix-org/typescript", "prettier"], + rules: { + // TypeScript has its own version of this + "babel/no-invalid-this": "off", + + quotes: "off", + }, + }, + { + files: ["src/interfaces/**/*.ts"], + rules: { + "@typescript-eslint/no-empty-object-type": "off", + }, + }, + ], +} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 77c1982..a7c35cd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,38 +1,38 @@ name: Build and test on: - push: - branches: - - master - pull_request: + push: + branches: + - master + pull_request: jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - cache: "yarn" - - - name: Install NPM packages - run: yarn install --frozen-lockfile - - - name: Check Linting Rules and Types - run: yarn lint - - - name: test - run: yarn test --coverage - - - name: Upload coverage - uses: actions/upload-artifact@v4 - with: - name: coverage - path: | - coverage - !coverage/lcov-report - - - name: build - run: yarn build + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + cache: "yarn" + + - name: Install NPM packages + run: yarn install --frozen-lockfile + + - name: Check Linting Rules and Types + run: yarn lint + + - name: test + run: yarn test --coverage + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: | + coverage + !coverage/lcov-report + + - name: build + run: yarn build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f862c2c..7f13a4d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,59 +1,59 @@ name: Release Automation on: - workflow_dispatch: - inputs: - version-bump: - description: The scale of the version bump required for semver compatibility - required: true - default: patch - type: choice - options: - - patch - - minor - - major + workflow_dispatch: + inputs: + version-bump: + description: The scale of the version bump required for semver compatibility + required: true + default: patch + type: choice + options: + - patch + - minor + - major concurrency: release permissions: - contents: write + contents: write jobs: - release: - name: "Release & Publish" - runs-on: ubuntu-latest - steps: - - name: 🧮 Checkout code - uses: actions/checkout@v4 - with: - token: ${{ secrets.ELEMENT_BOT_TOKEN }} + release: + name: "Release & Publish" + runs-on: ubuntu-latest + steps: + - name: 🧮 Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - name: 🔧 Set up node environment - uses: actions/setup-node@v4 - with: - cache: "yarn" + - name: 🔧 Set up node environment + uses: actions/setup-node@v4 + with: + cache: "yarn" - - name: 🛠️ Setup - run: yarn install --pure-lockfile + - name: 🛠️ Setup + run: yarn install --pure-lockfile - - name: 👊 Bump version - run: | - yarn version --no-git-tag-version --${{ github.event.inputs.version-bump }} - git config --global user.name 'ElementRobot' - git config --global user.email 'releases@riot.im' - git commit -am "${{ github.event.inputs.version-bump }} version bump" - git push + - name: 👊 Bump version + run: | + yarn version --no-git-tag-version --${{ github.event.inputs.version-bump }} + git config --global user.name 'ElementRobot' + git config --global user.email 'releases@riot.im' + git commit -am "${{ github.event.inputs.version-bump }} version bump" + git push - - name: 📖 Build lib - run: yarn build + - name: 📖 Build lib + run: yarn build - - name: 🚀 Publish to npm - id: npm-publish - uses: JS-DevTools/npm-publish@v3 - with: - token: ${{ secrets.NPM_TOKEN }} - access: public + - name: 🚀 Publish to npm + id: npm-publish + uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.NPM_TOKEN }} + access: public - - name: 🧬 Create release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.npm-publish.outputs.version }} - body: ${{ steps.npm-publish.outputs.version }} Release - draft: false - prerelease: false + - name: 🧬 Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.npm-publish.outputs.version }} + body: ${{ steps.npm-publish.outputs.version }} Release + draft: false + prerelease: false diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 2773eaa..c539966 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -1,16 +1,16 @@ name: SonarQube on: - workflow_run: - workflows: ["Build and test"] - types: - - completed + workflow_run: + workflows: ["Build and test"] + types: + - completed concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true jobs: - sonarqube: - name: 🩻 SonarQube - uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop - secrets: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + sonarqube: + name: 🩻 SonarQube + uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..257a7df --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "semi": false +} diff --git a/README.md b/README.md index 6f9b715..f092d98 100644 --- a/README.md +++ b/README.md @@ -36,40 +36,40 @@ to instantiate the `WidgetApi` class. The general usage for this would be: ```typescript -const widgetId = null; // if you know the widget ID, supply it. -const api = new WidgetApi(widgetId); +const widgetId = null // if you know the widget ID, supply it. +const api = new WidgetApi(widgetId) // Before doing anything else, request capabilities: -api.requestCapability(MatrixCapabilities.Screenshots); -api.requestCapabilities(StickerpickerCapabilities); +api.requestCapability(MatrixCapabilities.Screenshots) +api.requestCapabilities(StickerpickerCapabilities) // Add custom action handlers (if needed) api.on( - `action:${WidgetApiToWidgetAction.UpdateVisibility}`, - (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, {}); - }, -); + `action:${WidgetApiToWidgetAction.UpdateVisibility}`, + (ev: CustomEvent) => { + ev.preventDefault() // we're handling it, so stop the widget API from doing something. + console.log(ev.detail) // custom handling here + api.transport.reply(ev.detail, {}) + }, +) api.on( - "action:com.example.my_action", - (ev: CustomEvent) => { - ev.preventDefault(); // we're handling it, so stop the widget API from doing something. - console.log(ev.detail); // custom handling here - api.transport.reply(ev.detail, { custom: "reply" }); - }, -); + "action:com.example.my_action", + (ev: CustomEvent) => { + ev.preventDefault() // we're handling it, so stop the widget API from doing something. + console.log(ev.detail) // custom handling here + api.transport.reply(ev.detail, { custom: "reply" }) + }, +) // Start the messaging -api.start(); +api.start() // If waitForIframeLoad is false, tell the client that we're good to go -api.sendContentLoaded(); +api.sendContentLoaded() // Later, do something else (if needed) -api.setAlwaysOnScreen(true); -api.transport.send("com.example.my_action", { isExample: true }); +api.setAlwaysOnScreen(true) +api.transport.send("com.example.my_action", { isExample: true }) ``` For a more complete example, see the `examples` directory of this repo. @@ -83,17 +83,17 @@ SDK to provide an interface for other platforms. TODO: Improve this ```typescript -const driver = new CustomDriver(); // an implementation of WidgetDriver -const api = new ClientWidgetApi(widget, iframe, driver); +const driver = new CustomDriver() // an implementation of WidgetDriver +const api = new ClientWidgetApi(widget, iframe, driver) // The API is automatically started, so we just have to wait for a ready before doing something api.on("ready", () => { - api - .updateVisibility(true) - .then(() => console.log("Widget knows it is visible now")); - api.transport.send("com.example.my_action", { isExample: true }); -}); + api.updateVisibility(true).then(() => + console.log("Widget knows it is visible now"), + ) + api.transport.send("com.example.my_action", { isExample: true }) +}) // Eventually, stop the API handling -api.stop(); +api.stop() ``` diff --git a/examples/widget/index.css b/examples/widget/index.css index f8c9db1..18f9565 100644 --- a/examples/widget/index.css +++ b/examples/widget/index.css @@ -16,24 +16,24 @@ html, body { - background-color: #ffffff; - color: #000000; - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background-color: #ffffff; + color: #000000; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } body { - padding: 20px; + padding: 20px; } button { - border: none; - color: #ffffff; - background-color: #2a9d8f; - border-radius: 4px; - padding: 6px 12px; - cursor: pointer; + border: none; + color: #ffffff; + background-color: #2a9d8f; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; } #stickyState { - color: #3d5a80; + color: #3d5a80; } diff --git a/examples/widget/index.html b/examples/widget/index.html index d1f62cc..b1f8b73 100644 --- a/examples/widget/index.html +++ b/examples/widget/index.html @@ -15,10 +15,10 @@ --> - - Example Widget + + Example Widget - - - - - - -
Loading...
- - - - - - - - - - + + + + + +
Loading...
+ + + + + + + + + + diff --git a/examples/widget/utils.js b/examples/widget/utils.js index d83bd4a..daccb83 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,21 +15,21 @@ */ function parseFragment() { - const fragmentString = window.location.hash || "?"; - return new URLSearchParams( - fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0)), - ); + const fragmentString = window.location.hash || "?" + return new URLSearchParams( + fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0)), + ) } function assertParam(fragment, name) { - const val = fragment.get(name); - if (!val) - throw new Error(`${name} is not present in URL - cannot load widget`); - return val; + const val = fragment.get(name) + if (!val) + throw new Error(`${name} is not present in URL - cannot load widget`) + return val } function handleError(e) { - console.error(e); - document.getElementById("container").innerText = - "There was an error with the widget. See JS console for details."; + console.error(e) + document.getElementById("container").innerText = + "There was an error with the widget. See JS console for details." } diff --git a/jest.config.ts b/jest.config.ts index 99641ec..f9bd95b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -14,21 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { env } from "process"; +import { env } from "process" -import type { Config } from "jest"; +import type { Config } from "jest" const config: Config = { - testEnvironment: "jsdom", - testMatch: ["/test/**/*-test.[jt]s?(x)"], - collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], - coverageReporters: ["text-summary", "lcov"], - testResultsProcessor: "@casualbot/jest-sonar-reporter", -}; + testEnvironment: "jsdom", + testMatch: ["/test/**/*-test.[jt]s?(x)"], + collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], + coverageReporters: ["text-summary", "lcov"], + testResultsProcessor: "@casualbot/jest-sonar-reporter", +} // if we're running under GHA, enable the GHA reporter if (env["GITHUB_ACTIONS"] !== undefined) { - config.reporters = [["github-actions", { silent: false }], "summary"]; + config.reporters = [["github-actions", { silent: false }], "summary"] } -export default config; +export default config diff --git a/package.json b/package.json index cda642c..d5bccb0 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,79 @@ { - "name": "matrix-widget-api", - "version": "1.12.0", - "description": "Matrix Widget API SDK", - "main": "./lib/index.js", - "types": "./lib/index.d.ts", - "repository": "https://github.com/matrix-org/matrix-widget-api", - "author": "The Matrix.org Foundation C.I.C.", - "license": "Apache-2.0", - "scripts": { - "start": "tsc -w", - "clean": "rimraf lib dist", - "build": "yarn clean && yarn build:compile && yarn build:types && yarn build:browser", - "build:compile": "babel -d lib --verbose --extensions \".ts\" src", - "build:types": "tsc --emitDeclarationOnly", - "build:browser": "yarn build:browser:dev && yarn build:browser:prod", - "build:browser:dev": "browserify lib/index.js --debug --s mxwidgets -o dist/api.js", - "build:browser:prod": "browserify lib/index.js --s mxwidgets -p tinyify -o dist/api.min.js", - "lint": "yarn lint:types && yarn lint:ts && yarn lint:workflows", - "lint:ts": "eslint src test", - "lint:types": "tsc --noEmit", - "lint:fix": "eslint src test --fix", - "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", - "test": "jest" - }, - "files": [ - "src", - "lib", - "dist", - "package.json", - "README.md", - "LICENSE", - "CONTRIBUTING.rst" - ], - "devDependencies": { - "@action-validator/cli": "^0.6.0", - "@action-validator/core": "^0.5.3", - "@babel/cli": "^7.11.6", - "@babel/core": "^7.11.6", - "@babel/eslint-parser": "^7.25.9", - "@babel/eslint-plugin": "^7.25.9", - "@babel/plugin-proposal-class-properties": "^7.10.4", - "@babel/preset-env": "^7.11.5", - "@babel/preset-typescript": "^7.10.4", - "@casualbot/jest-sonar-reporter": "^2.2.7", - "@stylistic/eslint-plugin": "^2.10.1", - "@testing-library/dom": "^8.0.0", - "@types/jest": "^29.5.12", - "@types/node": "^18.16.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "browserify": "^17.0.0", - "eslint": "^8.0.0", - "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-matrix-org": "^2.0.0", - "eslint-plugin-unicorn": "^56.0.0", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "prettier": "3.4.2", - "rimraf": "^3.0.2", - "tinyify": "^3.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.0.4" - }, - "dependencies": { - "@types/events": "^3.0.0", - "events": "^3.2.0" - }, - "@casualbot/jest-sonar-reporter": { - "outputDirectory": "coverage", - "outputName": "jest-sonar-report.xml", - "relativePaths": true - } + "name": "matrix-widget-api", + "version": "1.12.0", + "description": "Matrix Widget API SDK", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "repository": "https://github.com/matrix-org/matrix-widget-api", + "author": "The Matrix.org Foundation C.I.C.", + "license": "Apache-2.0", + "scripts": { + "start": "tsc -w", + "clean": "rimraf lib dist", + "build": "yarn clean && yarn build:compile && yarn build:types && yarn build:browser", + "build:compile": "babel -d lib --verbose --extensions \".ts\" src", + "build:types": "tsc --emitDeclarationOnly", + "build:browser": "yarn build:browser:dev && yarn build:browser:prod", + "build:browser:dev": "browserify lib/index.js --debug --s mxwidgets -o dist/api.js", + "build:browser:prod": "browserify lib/index.js --s mxwidgets -p tinyify -o dist/api.min.js", + "lint": "yarn lint:types && yarn lint:ts && yarn lint:workflows", + "lint:ts": "eslint src test", + "lint:types": "tsc --noEmit", + "lint:fix": "eslint src test --fix", + "lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'", + "prettier:check": "prettier -c .", + "prettier:format": "prettier -w .", + "test": "jest" + }, + "files": [ + "src", + "lib", + "dist", + "package.json", + "README.md", + "LICENSE", + "CONTRIBUTING.rst" + ], + "devDependencies": { + "@action-validator/cli": "^0.6.0", + "@action-validator/core": "^0.5.3", + "@babel/cli": "^7.11.6", + "@babel/core": "^7.11.6", + "@babel/eslint-parser": "^7.25.9", + "@babel/eslint-plugin": "^7.25.9", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/preset-env": "^7.11.5", + "@babel/preset-typescript": "^7.10.4", + "@casualbot/jest-sonar-reporter": "^2.2.7", + "@stylistic/eslint-plugin": "^2.10.1", + "@testing-library/dom": "^8.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^18.16.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "browserify": "^17.0.0", + "eslint": "^8.0.0", + "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-matrix-org": "^2.0.0", + "eslint-plugin-unicorn": "^56.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "prettier": "3.4.2", + "rimraf": "^3.0.2", + "tinyify": "^3.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "dependencies": { + "@types/events": "^3.0.0", + "events": "^3.2.0" + }, + "@casualbot/jest-sonar-reporter": { + "outputDirectory": "coverage", + "outputName": "jest-sonar-report.xml", + "relativePaths": true + } } diff --git a/renovate.json b/renovate.json index 22a9943..d383718 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,4 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"] + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"] } diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index fcbb8c3..e6a9e82 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -14,123 +14,123 @@ * limitations under the License. */ -import { EventEmitter } from "events"; +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 { 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"; + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./interfaces/IWidgetApiRequest" +import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./interfaces/WidgetApiAction"; -import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./interfaces/WidgetApiAction" +import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse" import { - Capability, - MatrixCapabilities, - getTimelineRoomIDFromCapability, - isTimelineCapability, -} from "./interfaces/Capabilities"; + Capability, + MatrixCapabilities, + getTimelineRoomIDFromCapability, + isTimelineCapability, +} from "./interfaces/Capabilities" import { - IOpenIDUpdate, - ISendEventDetails, - ISendDelayedEventDetails, - WidgetDriver, -} from "./driver/WidgetDriver"; + IOpenIDUpdate, + ISendEventDetails, + ISendDelayedEventDetails, + WidgetDriver, +} from "./driver/WidgetDriver" import { - ICapabilitiesActionResponseData, - INotifyCapabilitiesActionRequestData, - IRenegotiateCapabilitiesActionRequest, -} from "./interfaces/CapabilitiesAction"; + 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"; + 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"; + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponseData, +} from "./interfaces/IWidgetApiResponse" import { - IModalWidgetButtonClickedRequestData, - IModalWidgetOpenRequestData, - IModalWidgetOpenRequestDataButton, - IModalWidgetReturnData, -} from "./interfaces/ModalWidgetActions"; + IModalWidgetButtonClickedRequestData, + IModalWidgetOpenRequestData, + IModalWidgetOpenRequestDataButton, + IModalWidgetReturnData, +} from "./interfaces/ModalWidgetActions" import { - ISendEventFromWidgetActionRequest, - ISendEventFromWidgetResponseData, - ISendEventToWidgetRequestData, -} from "./interfaces/SendEventAction"; + ISendEventFromWidgetActionRequest, + ISendEventFromWidgetResponseData, + ISendEventToWidgetRequestData, +} from "./interfaces/SendEventAction" import { - ISendToDeviceFromWidgetActionRequest, - ISendToDeviceFromWidgetResponseData, - ISendToDeviceToWidgetRequestData, -} from "./interfaces/SendToDeviceAction"; + ISendToDeviceFromWidgetActionRequest, + ISendToDeviceFromWidgetResponseData, + ISendToDeviceToWidgetRequestData, +} from "./interfaces/SendToDeviceAction" import { - EventDirection, - EventKind, - WidgetEventCapability, -} from "./models/WidgetEventCapability"; -import { IRoomEvent } from "./interfaces/IRoomEvent"; -import { IRoomAccountData } from "./interfaces/IRoomAccountData"; + EventDirection, + EventKind, + WidgetEventCapability, +} from "./models/WidgetEventCapability" +import { IRoomEvent } from "./interfaces/IRoomEvent" +import { IRoomAccountData } from "./interfaces/IRoomAccountData" import { - IGetOpenIDActionRequest, - IGetOpenIDActionResponseData, - IOpenIDCredentials, - OpenIDRequestState, -} from "./interfaces/GetOpenIDAction"; -import { SimpleObservable } from "./util/SimpleObservable"; -import { IOpenIDCredentialsActionRequestData } from "./interfaces/OpenIDCredentialsAction"; -import { INavigateActionRequest } from "./interfaces/NavigateAction"; + 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"; + IReadEventFromWidgetActionRequest, + IReadEventFromWidgetResponseData, +} from "./interfaces/ReadEventAction" import { - ITurnServer, - IWatchTurnServersRequest, - IUnwatchTurnServersRequest, - IUpdateTurnServersRequestData, -} from "./interfaces/TurnServerActions"; -import { Symbols } from "./Symbols"; + ITurnServer, + IWatchTurnServersRequest, + IUnwatchTurnServersRequest, + IUpdateTurnServersRequestData, +} from "./interfaces/TurnServerActions" +import { Symbols } from "./Symbols" import { - IReadRelationsFromWidgetActionRequest, - IReadRelationsFromWidgetResponseData, -} from "./interfaces/ReadRelationsAction"; + IReadRelationsFromWidgetActionRequest, + IReadRelationsFromWidgetResponseData, +} from "./interfaces/ReadRelationsAction" import { - IUserDirectorySearchFromWidgetActionRequest, - IUserDirectorySearchFromWidgetResponseData, -} from "./interfaces/UserDirectorySearchAction"; + IUserDirectorySearchFromWidgetActionRequest, + IUserDirectorySearchFromWidgetResponseData, +} from "./interfaces/UserDirectorySearchAction" import { - IReadRoomAccountDataFromWidgetActionRequest, - IReadRoomAccountDataFromWidgetResponseData, -} from "./interfaces/ReadRoomAccountDataAction"; + IReadRoomAccountDataFromWidgetActionRequest, + IReadRoomAccountDataFromWidgetResponseData, +} from "./interfaces/ReadRoomAccountDataAction" import { - IGetMediaConfigActionFromWidgetActionRequest, - IGetMediaConfigActionFromWidgetResponseData, -} from "./interfaces/GetMediaConfigAction"; + IGetMediaConfigActionFromWidgetActionRequest, + IGetMediaConfigActionFromWidgetResponseData, +} from "./interfaces/GetMediaConfigAction" import { - IUpdateDelayedEventFromWidgetActionRequest, - UpdateDelayedEventAction, -} from "./interfaces/UpdateDelayedEventAction"; + IUpdateDelayedEventFromWidgetActionRequest, + UpdateDelayedEventAction, +} from "./interfaces/UpdateDelayedEventAction" import { - IUploadFileActionFromWidgetActionRequest, - IUploadFileActionFromWidgetResponseData, -} from "./interfaces/UploadFileAction"; + IUploadFileActionFromWidgetActionRequest, + IUploadFileActionFromWidgetResponseData, +} from "./interfaces/UploadFileAction" import { - IDownloadFileActionFromWidgetActionRequest, - IDownloadFileActionFromWidgetResponseData, -} from "./interfaces/DownloadFileAction"; -import { IThemeChangeActionRequestData } from "./interfaces/ThemeChangeAction"; -import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction"; + IDownloadFileActionFromWidgetActionRequest, + IDownloadFileActionFromWidgetResponseData, +} from "./interfaces/DownloadFileAction" +import { IThemeChangeActionRequestData } from "./interfaces/ThemeChangeAction" +import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" /** * API handler for the client side of widgets. This raises events @@ -157,1310 +157,1406 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" * 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; - private contentLoadedWaitTimer?: ReturnType; - // Stores pending requests to push a room's state to the widget - private pushRoomStateTasks = new Set>(); - // Room ID → event type → state key → events to be pushed - private pushRoomStateResult = new Map< - string, - Map> - >(); - private flushRoomStateTask: Promise | 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 canReceiveRoomAccountData(eventType: string): boolean { - return this.allowedEvents.some((e) => - e.matchesAsRoomAccountData(EventDirection.Receive, eventType), - ); - } - - public stop(): void { - this.isStopped = true; - this.transport.stop(); - } - - private beginCapabilities(): void { - // 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) => { - this.allowCapabilities([...allowedCaps], requestedCaps); - this.emit("ready"); - }) - .catch((e) => { - this.emit("error:preparing", e); - }); - } - - private allowCapabilities(allowed: string[], requested: string[]): void { - console.log(`Widget ${this.widget.id} is allowed capabilities:`, allowed); - - for (const c of allowed) this.allowedCapabilities.add(c); - const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); - this.allowedEvents.push(...allowedEvents); - - this.transport - .send(WidgetApiToWidgetAction.NotifyCapabilities, < - INotifyCapabilitiesActionRequestData - >{ - requested, - approved: Array.from(this.allowedCapabilities), - }) - .catch((e) => { - console.warn( - "non-fatal error notifying widget of approved capabilities:", - e, - ); - }) - .then(() => { - this.emit("capabilitiesNotified"); - }); - - // Push the initial room state for all rooms with a timeline capability - for (const c of allowed) { - if (isTimelineCapability(c)) { - const roomId = getTimelineRoomIDFromCapability(c); - if (roomId === Symbols.AnyRoom) { - for (const roomId of this.driver.getKnownRooms()) - this.pushRoomState(roomId); - } else { - this.pushRoomState(roomId); - } - } - } - // If new events are allowed and the currently viewed room isn't covered - // by a timeline capability, then we know that there could be some state - // in the viewed room that the widget hasn't learned about yet- push it. - if ( - allowedEvents.length > 0 && - this.viewedRoomId !== null && - !this.canUseRoomTimeline(this.viewedRoomId) + 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 + private contentLoadedWaitTimer?: ReturnType + // Stores pending requests to push a room's state to the widget + private pushRoomStateTasks = new Set>() + // Room ID → event type → state key → events to be pushed + private pushRoomStateResult = new Map< + string, + Map> + >() + private flushRoomStateTask: Promise | 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, ) { - this.pushRoomState(this.viewedRoomId); + 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() } - } - - private onIframeLoad(ev: Event): void { - 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. - console.log( - "waitForIframeLoad is false: waiting for widget to send contentLoaded", - ); - this.contentLoadedWaitTimer = setTimeout(() => { - console.error( - "Widget specified waitForIframeLoad=false but timed out waiting for contentLoaded event!", - ); - }, 10000); - this.contentLoadedActionSent = false; + + public hasCapability(capability: Capability): boolean { + return this.allowedCapabilities.has(capability) } - } - private handleContentLoadedAction(action: IContentLoadedActionRequest): void { - if (this.contentLoadedWaitTimer !== undefined) { - clearTimeout(this.contentLoadedWaitTimer); - this.contentLoadedWaitTimer = undefined; + public canUseRoomTimeline(roomId: string | Symbols.AnyRoom): boolean { + return ( + this.hasCapability( + `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, + ) || this.hasCapability(`org.matrix.msc2762.timeline:${roomId}`) + ) } - if (this.contentLoadedActionSent) { - throw new Error( - "Improper sequence: ContentLoaded Action can only be sent once after the widget loaded " + - "and should only be used if waitForIframeLoad is false (default=true)", - ); + + public canSendRoomEvent( + eventType: string, + msgtype: string | null = null, + ): boolean { + return this.allowedEvents.some((e) => + e.matchesAsRoomEvent(EventDirection.Send, eventType, msgtype), + ) } - if (this.widget.waitForIframeLoad) { - this.transport.reply(action, { - error: { - message: - "Improper sequence: not expecting ContentLoaded event if " + - "waitForIframeLoad is true (default=true)", - }, - }); - } else { - this.transport.reply(action, {}); - this.beginCapabilities(); + + public canSendStateEvent(eventType: string, stateKey: string): boolean { + return this.allowedEvents.some((e) => + e.matchesAsStateEvent(EventDirection.Send, eventType, stateKey), + ) } - this.contentLoadedActionSent = true; - } - - private replyVersions(request: ISupportedVersionsActionRequest): void { - this.transport.reply(request, { - supported_versions: CurrentApiVersions, - }); - } - - private handleCapabilitiesRenegotiate( - request: IRenegotiateCapabilitiesActionRequest, - ): void { - // 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 - skip validation - this.allowCapabilities([], []); + + public canSendToDeviceEvent(eventType: string): boolean { + return this.allowedEvents.some((e) => + e.matchesAsToDeviceEvent(EventDirection.Send, eventType), + ) } - this.driver - .validateCapabilities(newlyRequested) - .then((allowed) => - this.allowCapabilities([...allowed], [...newlyRequested]), - ); - } - - private handleNavigate(request: INavigateActionRequest): void { - if (!this.hasCapability(MatrixCapabilities.MSC2931Navigate)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + public canReceiveRoomEvent( + eventType: string, + msgtype: string | null = null, + ): boolean { + return this.allowedEvents.some((e) => + e.matchesAsRoomEvent(EventDirection.Receive, eventType, msgtype), + ) } - if ( - !request.data?.uri || - !request.data?.uri.toString().startsWith("https://matrix.to/#") - ) { - return this.transport.reply(request, { - error: { message: "Invalid matrix.to URI" }, - }); + public canReceiveStateEvent( + eventType: string, + stateKey: string | null, + ): boolean { + return this.allowedEvents.some((e) => + e.matchesAsStateEvent(EventDirection.Receive, eventType, stateKey), + ) } - const onErr = (e: unknown): void => { - console.error("[ClientWidgetApi] Failed to handle navigation: ", e); - this.handleDriverError(e, request, "Error handling navigation"); - }; - - try { - this.driver - .navigate(request.data.uri.toString()) - .catch((e: unknown) => onErr(e)) - .then(() => { - return this.transport.reply( - request, - {}, - ); - }); - } catch (e) { - return onErr(e); + public canReceiveToDeviceEvent(eventType: string): boolean { + return this.allowedEvents.some((e) => + e.matchesAsToDeviceEvent(EventDirection.Receive, eventType), + ) } - } - - private handleOIDC(request: IGetOpenIDActionRequest): void { - let phase = 1; // 1 = initial request, 2 = after user manual confirmation - - const replyState = ( - state: OpenIDRequestState, - credential?: IOpenIDCredentials, - ): void | Promise => { - 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, - ): void | Promise => { - 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 handleReadRoomAccountData( - request: IReadRoomAccountDataFromWidgetActionRequest, - ): void | Promise { - let events: Promise = Promise.resolve([]); - events = this.driver.readRoomAccountData(request.data.type); - - if (!this.canReceiveRoomAccountData(request.data.type)) { - return this.transport.reply(request, { - error: { message: "Cannot read room account data of this type" }, - }); + + public canReceiveRoomAccountData(eventType: string): boolean { + return this.allowedEvents.some((e) => + e.matchesAsRoomAccountData(EventDirection.Receive, eventType), + ) } - return events.then((evs) => { - this.transport.reply( - request, - { events: evs }, - ); - }); - } - - private async handleReadEvents( - request: IReadEventFromWidgetActionRequest, - ): Promise { - if (!request.data.type) { - return this.transport.reply(request, { - error: { message: "Invalid request - missing event type" }, - }); + public stop(): void { + this.isStopped = true + this.transport.stop() } - 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" }, - }); + + private beginCapabilities(): void { + // 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) => { + this.allowCapabilities([...allowedCaps], requestedCaps) + this.emit("ready") + }) + .catch((e) => { + this.emit("error:preparing", e) + }) } - let askRoomIds: string[]; - if (request.data.room_ids === undefined) { - askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId]; - } else if (request.data.room_ids === Symbols.AnyRoom) { - askRoomIds = this.driver - .getKnownRooms() - .filter((roomId) => this.canUseRoomTimeline(roomId)); - } else { - askRoomIds = request.data.room_ids; - for (const roomId of askRoomIds) { - if (!this.canUseRoomTimeline(roomId)) { - return this.transport.reply(request, { - error: { message: `Unable to access room timeline: ${roomId}` }, - }); + private allowCapabilities(allowed: string[], requested: string[]): void { + console.log( + `Widget ${this.widget.id} is allowed capabilities:`, + allowed, + ) + + for (const c of allowed) this.allowedCapabilities.add(c) + const allowedEvents = + WidgetEventCapability.findEventCapabilities(allowed) + this.allowedEvents.push(...allowedEvents) + + this.transport + .send(WidgetApiToWidgetAction.NotifyCapabilities, < + INotifyCapabilitiesActionRequestData + >{ + requested, + approved: Array.from(this.allowedCapabilities), + }) + .catch((e) => { + console.warn( + "non-fatal error notifying widget of approved capabilities:", + e, + ) + }) + .then(() => { + this.emit("capabilitiesNotified") + }) + + // Push the initial room state for all rooms with a timeline capability + for (const c of allowed) { + if (isTimelineCapability(c)) { + const roomId = getTimelineRoomIDFromCapability(c) + if (roomId === Symbols.AnyRoom) { + for (const roomId of this.driver.getKnownRooms()) + this.pushRoomState(roomId) + } else { + this.pushRoomState(roomId) + } + } + } + // If new events are allowed and the currently viewed room isn't covered + // by a timeline capability, then we know that there could be some state + // in the viewed room that the widget hasn't learned about yet- push it. + if ( + allowedEvents.length > 0 && + this.viewedRoomId !== null && + !this.canUseRoomTimeline(this.viewedRoomId) + ) { + this.pushRoomState(this.viewedRoomId) } - } } - const limit = request.data.limit || 0; - const since = request.data.since; - - let stateKey: string | undefined = undefined; - let msgtype: string | undefined = undefined; - if (request.data.state_key !== undefined) { - 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" }, - }); - } - } else { - msgtype = request.data.msgtype; - if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { - return this.transport.reply(request, { - error: { message: "Cannot read room events of this type" }, - }); - } + private onIframeLoad(ev: Event): void { + 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. + console.log( + "waitForIframeLoad is false: waiting for widget to send contentLoaded", + ) + this.contentLoadedWaitTimer = setTimeout(() => { + console.error( + "Widget specified waitForIframeLoad=false but timed out waiting for contentLoaded event!", + ) + }, 10000) + this.contentLoadedActionSent = false + } } - // For backwards compatibility we still call the deprecated - // readRoomEvents and readStateEvents methods in case the client isn't - // letting us know the currently viewed room via setViewedRoomId - const events = - request.data.room_ids === undefined && askRoomIds.length === 0 - ? await (request.data.state_key === undefined - ? this.driver.readRoomEvents( - request.data.type, - msgtype, - limit, - null, - since, - ) - : this.driver.readStateEvents( - request.data.type, - stateKey, - limit, - null, - )) - : ( - await Promise.all( - askRoomIds.map((roomId) => - this.driver.readRoomTimeline( - roomId, - request.data.type, - msgtype, - stateKey, - limit, - since, - ), - ), + private handleContentLoadedAction( + action: IContentLoadedActionRequest, + ): void { + if (this.contentLoadedWaitTimer !== undefined) { + clearTimeout(this.contentLoadedWaitTimer) + this.contentLoadedWaitTimer = undefined + } + if (this.contentLoadedActionSent) { + throw new Error( + "Improper sequence: ContentLoaded Action can only be sent once after the widget loaded " + + "and should only be used if waitForIframeLoad is false (default=true)", ) - ).flat(1); - this.transport.reply(request, { events }); - } - - private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { - if (!request.data.type) { - return this.transport.reply(request, { - error: { message: "Invalid request - missing event type" }, - }); + } + if (this.widget.waitForIframeLoad) { + this.transport.reply(action, { + error: { + message: + "Improper sequence: not expecting ContentLoaded event if " + + "waitForIframeLoad is true (default=true)", + }, + }) + } else { + this.transport.reply(action, {}) + this.beginCapabilities() + } + this.contentLoadedActionSent = true } - 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}`, - }, - }); + private replyVersions(request: ISupportedVersionsActionRequest): void { + this.transport.reply(request, { + supported_versions: CurrentApiVersions, + }) } - const isDelayedEvent = - request.data.delay !== undefined || - request.data.parent_delay_id !== undefined; - if ( - isDelayedEvent && - !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent) - ) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + private handleCapabilitiesRenegotiate( + request: IRenegotiateCapabilitiesActionRequest, + ): void { + // 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 - skip validation + this.allowCapabilities([], []) + } + + this.driver + .validateCapabilities(newlyRequested) + .then((allowed) => + this.allowCapabilities([...allowed], [...newlyRequested]), + ) } - let sendEventPromise: Promise; - if (request.data.state_key !== undefined) { - if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { - return this.transport.reply(request, { - error: { message: "Cannot send state events of this type" }, - }); - } - - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( - request.data.type, - request.data.content || {}, - request.data.state_key, - request.data.room_id, - ); - } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, - 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" }, - }); - } - - if (!isDelayedEvent) { - sendEventPromise = this.driver.sendEvent( - request.data.type, - content, - null, // not sending a state event - request.data.room_id, - ); - } else { - sendEventPromise = this.driver.sendDelayedEvent( - request.data.delay ?? null, - request.data.parent_delay_id ?? null, - request.data.type, - content, - null, // not sending a state event - request.data.room_id, - ); - } + private handleNavigate(request: INavigateActionRequest): void { + 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: unknown): void => { + console.error("[ClientWidgetApi] Failed to handle navigation: ", e) + this.handleDriverError(e, request, "Error handling navigation") + } + + try { + this.driver + .navigate(request.data.uri.toString()) + .catch((e: unknown) => onErr(e)) + .then(() => { + return this.transport.reply( + request, + {}, + ) + }) + } catch (e) { + return onErr(e) + } } - sendEventPromise - .then((sentEvent) => { - return this.transport.reply(request, { - room_id: sentEvent.roomId, - ...("eventId" in sentEvent - ? { - event_id: sentEvent.eventId, - } - : { - delay_id: sentEvent.delayId, - }), - }); - }) - .catch((e: unknown) => { - console.error("error sending event: ", e); - this.handleDriverError(e, request, "Error sending event"); - }); - } - - private handleUpdateDelayedEvent( - request: IUpdateDelayedEventFromWidgetActionRequest, - ): void { - if (!request.data.delay_id) { - return this.transport.reply(request, { - error: { message: "Invalid request - missing delay_id" }, - }); + private handleOIDC(request: IGetOpenIDActionRequest): void { + let phase = 1 // 1 = initial request, 2 = after user manual confirmation + + const replyState = ( + state: OpenIDRequestState, + credential?: IOpenIDCredentials, + ): void | Promise => { + 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, + ): void | Promise => { + 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 handleReadRoomAccountData( + request: IReadRoomAccountDataFromWidgetActionRequest, + ): void | Promise { + let events: Promise = Promise.resolve([]) + events = this.driver.readRoomAccountData(request.data.type) + + if (!this.canReceiveRoomAccountData(request.data.type)) { + return this.transport.reply(request, { + error: { + message: "Cannot read room account data of this type", + }, + }) + } - if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + return events.then((evs) => { + this.transport.reply( + request, + { events: evs }, + ) + }) } - switch (request.data.action) { - case UpdateDelayedEventAction.Cancel: - case UpdateDelayedEventAction.Restart: - case UpdateDelayedEventAction.Send: - this.driver - .updateDelayedEvent(request.data.delay_id, request.data.action) - .then(() => { - return this.transport.reply( - request, - {}, - ); - }) - .catch((e: unknown) => { - console.error("error updating delayed event: ", e); - this.handleDriverError(e, request, "Error updating delayed event"); - }); - break; - default: - return this.transport.reply(request, { - error: { message: "Invalid request - unsupported action" }, - }); + private async handleReadEvents( + request: IReadEventFromWidgetActionRequest, + ): Promise { + 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[] + if (request.data.room_ids === undefined) { + askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId] + } else if (request.data.room_ids === Symbols.AnyRoom) { + askRoomIds = this.driver + .getKnownRooms() + .filter((roomId) => this.canUseRoomTimeline(roomId)) + } else { + askRoomIds = request.data.room_ids + 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 + const since = request.data.since + + let stateKey: string | undefined = undefined + let msgtype: string | undefined = undefined + if (request.data.state_key !== undefined) { + 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", + }, + }, + ) + } + } else { + msgtype = request.data.msgtype + if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { + return this.transport.reply( + request, + { + error: { + message: "Cannot read room events of this type", + }, + }, + ) + } + } + + // For backwards compatibility we still call the deprecated + // readRoomEvents and readStateEvents methods in case the client isn't + // letting us know the currently viewed room via setViewedRoomId + const events = + request.data.room_ids === undefined && askRoomIds.length === 0 + ? await (request.data.state_key === undefined + ? this.driver.readRoomEvents( + request.data.type, + msgtype, + limit, + null, + since, + ) + : this.driver.readStateEvents( + request.data.type, + stateKey, + limit, + null, + )) + : ( + await Promise.all( + askRoomIds.map((roomId) => + this.driver.readRoomTimeline( + roomId, + request.data.type, + msgtype, + stateKey, + limit, + since, + ), + ), + ) + ).flat(1) + this.transport.reply(request, { + events, + }) } - } - - 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); - this.handleDriverError(e, request, "Error sending event"); - } + + private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { + 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 isDelayedEvent = + request.data.delay !== undefined || + request.data.parent_delay_id !== undefined + if ( + isDelayedEvent && + !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent) + ) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }) + } + + let sendEventPromise: Promise< + ISendEventDetails | ISendDelayedEventDetails + > + if (request.data.state_key !== undefined) { + if ( + !this.canSendStateEvent( + request.data.type, + request.data.state_key, + ) + ) { + return this.transport.reply( + request, + { + error: { + message: "Cannot send state events of this type", + }, + }, + ) + } + + if (!isDelayedEvent) { + sendEventPromise = this.driver.sendEvent( + request.data.type, + request.data.content || {}, + request.data.state_key, + request.data.room_id, + ) + } else { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, + 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", + }, + }, + ) + } + + if (!isDelayedEvent) { + sendEventPromise = this.driver.sendEvent( + request.data.type, + content, + null, // not sending a state event + request.data.room_id, + ) + } else { + sendEventPromise = this.driver.sendDelayedEvent( + request.data.delay ?? null, + request.data.parent_delay_id ?? null, + 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, + ...("eventId" in sentEvent + ? { + event_id: sentEvent.eventId, + } + : { + delay_id: sentEvent.delayId, + }), + }, + ) + }) + .catch((e: unknown) => { + console.error("error sending event: ", e) + this.handleDriverError(e, request, "Error sending event") + }) } - } - - private async pollTurnServers( - turnServers: AsyncGenerator, - initialServer: ITurnServer, - ): Promise { - 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 handleUpdateDelayedEvent( + request: IUpdateDelayedEventFromWidgetActionRequest, + ): void { + if (!request.data.delay_id) { + return this.transport.reply(request, { + error: { message: "Invalid request - missing delay_id" }, + }) + } + + if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }) + } + + switch (request.data.action) { + case UpdateDelayedEventAction.Cancel: + case UpdateDelayedEventAction.Restart: + case UpdateDelayedEventAction.Send: + this.driver + .updateDelayedEvent( + request.data.delay_id, + request.data.action, + ) + .then(() => { + return this.transport.reply( + request, + {}, + ) + }) + .catch((e: unknown) => { + console.error("error updating delayed event: ", e) + this.handleDriverError( + e, + request, + "Error updating delayed event", + ) + }) + break + default: + return this.transport.reply( + request, + { + error: { + message: "Invalid request - unsupported action", + }, + }, + ) + } } - } - - 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 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) + this.handleDriverError(e, request, "Error sending event") + } + } } - } - - 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 pollTurnServers( + turnServers: AsyncGenerator, + initialServer: ITurnServer, + ): Promise { + 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 handleReadRelations( - request: IReadRelationsFromWidgetActionRequest, - ): Promise { - if (!request.data.event_id) { - return this.transport.reply(request, { - error: { message: "Invalid request - missing event ID" }, - }); + + 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" }, + }, + ) + } + } } - if (request.data.limit !== undefined && request.data.limit < 0) { - return this.transport.reply(request, { - error: { message: "Invalid request - limit out of range" }, - }); + 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, + {}, + ) + } } - 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}`, - }, - }); + private async handleReadRelations( + request: IReadRelationsFromWidgetActionRequest, + ): Promise { + 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) + this.handleDriverError( + e, + request, + "Unexpected error while reading relations", + ) + } } - 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"], - ); + private async handleUserDirectorySearch( + request: IUserDirectorySearchFromWidgetActionRequest, + ): Promise { + 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) + this.handleDriverError( + e, + request, + "Unexpected error while searching in the user directory", + ) } - }); - - return this.transport.reply( - request, - { - chunk, - prev_batch: result.prevBatch, - next_batch: result.nextBatch, - }, - ); - } catch (e) { - console.error("error getting the relations", e); - this.handleDriverError( - e, - request, - "Unexpected error while reading relations", - ); } - } - - private async handleUserDirectorySearch( - request: IUserDirectorySearchFromWidgetActionRequest, - ): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC3973UserDirectorySearch)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + + private async handleGetMediaConfig( + request: IGetMediaConfigActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }) + } + + try { + const result = await this.driver.getMediaConfig() + + return this.transport.reply( + request, + result, + ) + } catch (e) { + console.error("error while getting the media configuration", e) + this.handleDriverError( + e, + request, + "Unexpected error while getting the media configuration", + ) + } } - if (typeof request.data.search_term !== "string") { - return this.transport.reply(request, { - error: { message: "Invalid request - missing search term" }, - }); + private async handleUploadFile( + request: IUploadFileActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }) + } + + try { + const result = await this.driver.uploadFile(request.data.file) + + return this.transport.reply( + request, + { + content_uri: result.contentUri, + }, + ) + } catch (e) { + console.error("error while uploading a file", e) + this.handleDriverError( + e, + request, + "Unexpected error while uploading a file", + ) + } } - if (request.data.limit !== undefined && request.data.limit < 0) { - return this.transport.reply(request, { - error: { message: "Invalid request - limit out of range" }, - }); + private async handleDownloadFile( + request: IDownloadFileActionFromWidgetActionRequest, + ): Promise { + if (!this.hasCapability(MatrixCapabilities.MSC4039DownloadFile)) { + return this.transport.reply(request, { + error: { message: "Missing capability" }, + }) + } + + try { + const result = await this.driver.downloadFile( + request.data.content_uri, + ) + + return this.transport.reply( + request, + { file: result.file }, + ) + } catch (e) { + console.error("error while downloading a file", e) + this.handleDriverError( + e, + request, + "Unexpected error while downloading a file", + ) + } } - 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); - this.handleDriverError( - e, - request, - "Unexpected error while searching in the user directory", - ); + private handleDriverError( + e: unknown, + request: IWidgetApiRequest, + message: string, + ): void { + const data = this.driver.processError(e) + this.transport.reply(request, { + error: { + message, + ...data, + }, + }) } - } - - private async handleGetMediaConfig( - request: IGetMediaConfigActionFromWidgetActionRequest, - ): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + + private handleMessage( + ev: CustomEvent, + ): void | Promise { + 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, + ) + case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: + return this.handleReadRoomAccountData( + ev.detail, + ) + case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: + return this.handleGetMediaConfig( + ev.detail, + ) + case WidgetApiFromWidgetAction.MSC4039UploadFileAction: + return this.handleUploadFile( + ev.detail, + ) + case WidgetApiFromWidgetAction.MSC4039DownloadFileAction: + return this.handleDownloadFile( + ev.detail, + ) + case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent: + return this.handleUpdateDelayedEvent( + ev.detail, + ) + + default: + return this.transport.reply(ev.detail, < + IWidgetApiErrorResponseData + >{ + error: { + message: + "Unknown or unsupported action: " + + ev.detail.action, + }, + }) + } + } } - try { - const result = await this.driver.getMediaConfig(); - - return this.transport.reply( - request, - result, - ); - } catch (e) { - console.error("error while getting the media configuration", e); - this.handleDriverError( - e, - request, - "Unexpected error while getting the media configuration", - ); + /** + * Informs the widget that the client's theme has changed. + * @param theme The theme data, as an object with arbitrary contents. + */ + public updateTheme( + theme: IThemeChangeActionRequestData, + ): Promise { + return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme) } - } - - private async handleUploadFile( - request: IUploadFileActionFromWidgetActionRequest, - ): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + + /** + * Informs the widget that the client's language has changed. + * @param lang The BCP 47 identifier representing the client's current language. + */ + public updateLanguage(lang: string): Promise { + return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { + lang, + }) } - try { - const result = await this.driver.uploadFile(request.data.file); - - return this.transport.reply( - request, - { - content_uri: result.contentUri, - }, - ); - } catch (e) { - console.error("error while uploading a file", e); - this.handleDriverError( - e, - request, - "Unexpected error while uploading a file", - ); + /** + * 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, + {}, + ) } - } - - private async handleDownloadFile( - request: IDownloadFileActionFromWidgetActionRequest, - ): Promise { - if (!this.hasCapability(MatrixCapabilities.MSC4039DownloadFile)) { - return this.transport.reply(request, { - error: { message: "Missing capability" }, - }); + + /** + * 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, < + IVisibilityActionRequestData + >{ + visible: isVisible, + }) } - try { - const result = await this.driver.downloadFile(request.data.content_uri); - - return this.transport.reply( - request, - { file: result.file }, - ); - } catch (e) { - console.error("error while downloading a file", e); - this.handleDriverError( - e, - request, - "Unexpected error while downloading a file", - ); + public sendWidgetConfig(data: IModalWidgetOpenRequestData): Promise { + return this.transport + .send( + WidgetApiToWidgetAction.WidgetConfig, + data, + ) + .then() } - } - - private handleDriverError( - e: unknown, - request: IWidgetApiRequest, - message: string, - ): void { - const data = this.driver.processError(e); - this.transport.reply(request, { - error: { - message, - ...data, - }, - }); - } - - private handleMessage( - ev: CustomEvent, - ): void | Promise { - 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, - ); - case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: - return this.handleReadRoomAccountData( - ev.detail, - ); - case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: - return this.handleGetMediaConfig( - ev.detail, - ); - case WidgetApiFromWidgetAction.MSC4039UploadFileAction: - return this.handleUploadFile( - ev.detail, - ); - case WidgetApiFromWidgetAction.MSC4039DownloadFileAction: - return this.handleDownloadFile( - ev.detail, - ); - case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent: - return this.handleUpdateDelayedEvent( - ev.detail, - ); - - default: - return this.transport.reply(ev.detail, { - error: { - message: "Unknown or unsupported action: " + ev.detail.action, - }, - }); - } + + public notifyModalWidgetButtonClicked( + id: IModalWidgetOpenRequestDataButton["id"], + ): Promise { + return this.transport + .send( + WidgetApiToWidgetAction.ButtonClicked, + { id }, + ) + .then() } - } - - /** - * Informs the widget that the client's theme has changed. - * @param theme The theme data, as an object with arbitrary contents. - */ - public updateTheme( - theme: IThemeChangeActionRequestData, - ): Promise { - return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme); - } - - /** - * Informs the widget that the client's language has changed. - * @param lang The BCP 47 identifier representing the client's current language. - */ - public updateLanguage(lang: string): Promise { - return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { - lang, - }); - } - - /** - * 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, < - IVisibilityActionRequestData - >{ - 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. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. - * @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 delivered or if the widget is not - * able to read the event due to permissions, rejects if the widget failed - * to handle the event. - * @deprecated It is recommended to communicate the viewed room ID by calling - * {@link ClientWidgetApi.setViewedRoomId} rather than passing it to this - * method. - */ - public async feedEvent( - rawEvent: IRoomEvent, - currentViewedRoomId: string, - ): Promise; - /** - * Feeds an event to the widget. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. - * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. - * @returns {Promise} Resolves when delivered or if the widget is not - * able to read the event due to permissions, rejects if the widget failed - * to handle the event. - */ - public async feedEvent(rawEvent: IRoomEvent): Promise; - public async feedEvent( - rawEvent: IRoomEvent, - currentViewedRoomId?: string, - ): Promise { - if (currentViewedRoomId !== undefined) - this.setViewedRoomId(currentViewedRoomId); - if ( - rawEvent.room_id !== this.viewedRoomId && - !this.canUseRoomTimeline(rawEvent.room_id) - ) { - return; // no-op + + public notifyModalWidgetClose(data: IModalWidgetReturnData): Promise { + return this.transport + .send( + WidgetApiToWidgetAction.CloseModalWidget, + data, + ) + .then() } - 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"], + /** + * Feeds an event to the widget. As a client you are expected to call this + * for every new event in every room to which you are joined or invited. + * @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 delivered or if the widget is not + * able to read the event due to permissions, rejects if the widget failed + * to handle the event. + * @deprecated It is recommended to communicate the viewed room ID by calling + * {@link ClientWidgetApi.setViewedRoomId} rather than passing it to this + * method. + */ + public async feedEvent( + rawEvent: IRoomEvent, + currentViewedRoomId: string, + ): Promise + /** + * Feeds an event to the widget. As a client you are expected to call this + * for every new event in every room to which you are joined or invited. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @returns {Promise} Resolves when delivered or if the widget is not + * able to read the event due to permissions, rejects if the widget failed + * to handle the event. + */ + public async feedEvent(rawEvent: IRoomEvent): Promise + public async feedEvent( + rawEvent: IRoomEvent, + currentViewedRoomId?: string, + ): Promise { + if (currentViewedRoomId !== undefined) + this.setViewedRoomId(currentViewedRoomId) + if ( + rawEvent.room_id !== this.viewedRoomId && + !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, + // it's compatible, but missing the index signature + rawEvent as ISendEventToWidgetRequestData, ) - ) { - return; // no-op - } } - // Feed the event into the widget - await this.transport.send( - WidgetApiToWidgetAction.SendEvent, - // it's compatible, but missing the index signature - rawEvent as ISendEventToWidgetRequestData, - ); - } - - /** - * Feeds a to-device event to the widget. As a client you are expected to - * call this for every to-device event you receive. - * @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 delivered or if the widget is not - * able to receive the event due to permissions, rejects if the widget - * failed to handle the event. - */ - 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, - ); - } - } - - private viewedRoomId: string | null = null; - - /** - * Indicate that a room is being viewed (making it possible for the widget - * to interact with it). - */ - public setViewedRoomId(roomId: string | null): void { - this.viewedRoomId = roomId; - // If the widget doesn't have timeline permissions for the room then - // this is its opportunity to learn the room state. We push the entire - // room state, which could be redundant if this room had been viewed - // once before, but it's easier than selectively pushing just the bits - // of state that changed while the room was in the background. - if (roomId !== null && !this.canUseRoomTimeline(roomId)) - this.pushRoomState(roomId); - } - - private async flushRoomState(): Promise { - try { - // Only send a single action once all concurrent tasks have completed - do await Promise.all([...this.pushRoomStateTasks]); - while (this.pushRoomStateTasks.size > 0); - - const events: IRoomEvent[] = []; - for (const eventTypeMap of this.pushRoomStateResult.values()) { - for (const stateKeyMap of eventTypeMap.values()) { - events.push(...stateKeyMap.values()); + /** + * Feeds a to-device event to the widget. As a client you are expected to + * call this for every to-device event you receive. + * @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 delivered or if the widget is not + * able to receive the event due to permissions, rejects if the widget + * failed to handle the event. + */ + 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, + ) } - } - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { - state: events, - }, - ); - } finally { - this.flushRoomStateTask = null; } - } - - /** - * Read the room's state and push all entries that the widget is allowed to - * read through to the widget. - */ - private pushRoomState(roomId: string): void { - for (const cap of this.allowedEvents) { - if ( - cap.kind === EventKind.State && - cap.direction === EventDirection.Receive - ) { - // Initiate the task - const events = this.driver.readRoomState( - roomId, - cap.eventType, - cap.keyStr ?? undefined, - ); - const task = events - .then( - (events) => { - // When complete, queue the resulting events to be - // pushed to the widget - for (const event of events) { - let eventTypeMap = this.pushRoomStateResult.get(roomId); - if (eventTypeMap === undefined) { - eventTypeMap = new Map(); - this.pushRoomStateResult.set(roomId, eventTypeMap); - } - let stateKeyMap = eventTypeMap.get(cap.eventType); - if (stateKeyMap === undefined) { - stateKeyMap = new Map(); - eventTypeMap.set(cap.eventType, stateKeyMap); + + private viewedRoomId: string | null = null + + /** + * Indicate that a room is being viewed (making it possible for the widget + * to interact with it). + */ + public setViewedRoomId(roomId: string | null): void { + this.viewedRoomId = roomId + // If the widget doesn't have timeline permissions for the room then + // this is its opportunity to learn the room state. We push the entire + // room state, which could be redundant if this room had been viewed + // once before, but it's easier than selectively pushing just the bits + // of state that changed while the room was in the background. + if (roomId !== null && !this.canUseRoomTimeline(roomId)) + this.pushRoomState(roomId) + } + + private async flushRoomState(): Promise { + try { + // Only send a single action once all concurrent tasks have completed + do await Promise.all([...this.pushRoomStateTasks]) + while (this.pushRoomStateTasks.size > 0) + + const events: IRoomEvent[] = [] + for (const eventTypeMap of this.pushRoomStateResult.values()) { + for (const stateKeyMap of eventTypeMap.values()) { + events.push(...stateKeyMap.values()) } - if (!stateKeyMap.has(event.state_key!)) - stateKeyMap.set(event.state_key!, event); - } - }, - (e) => - console.error( - `Failed to read room state for ${roomId} (${cap.eventType}, ${cap.keyStr})`, - e, - ), - ) - .then(() => { - // Mark request as no longer pending - this.pushRoomStateTasks.delete(task); - }); - - // Mark task as pending - this.pushRoomStateTasks.add(task); - // Assuming no other tasks are already happening concurrently, - // schedule the widget action that actually pushes the events - this.flushRoomStateTask ??= this.flushRoomState(); - this.flushRoomStateTask.catch((e) => - console.error("Failed to push room state", e), - ); - } + } + await this.transport.send( + WidgetApiToWidgetAction.UpdateState, + { + state: events, + }, + ) + } finally { + this.flushRoomStateTask = null + } + } + + /** + * Read the room's state and push all entries that the widget is allowed to + * read through to the widget. + */ + private pushRoomState(roomId: string): void { + for (const cap of this.allowedEvents) { + if ( + cap.kind === EventKind.State && + cap.direction === EventDirection.Receive + ) { + // Initiate the task + const events = this.driver.readRoomState( + roomId, + cap.eventType, + cap.keyStr ?? undefined, + ) + const task = events + .then( + (events) => { + // When complete, queue the resulting events to be + // pushed to the widget + for (const event of events) { + let eventTypeMap = + this.pushRoomStateResult.get(roomId) + if (eventTypeMap === undefined) { + eventTypeMap = new Map() + this.pushRoomStateResult.set( + roomId, + eventTypeMap, + ) + } + let stateKeyMap = eventTypeMap.get( + cap.eventType, + ) + if (stateKeyMap === undefined) { + stateKeyMap = new Map() + eventTypeMap.set(cap.eventType, stateKeyMap) + } + if (!stateKeyMap.has(event.state_key!)) + stateKeyMap.set(event.state_key!, event) + } + }, + (e) => + console.error( + `Failed to read room state for ${roomId} (${cap.eventType}, ${cap.keyStr})`, + e, + ), + ) + .then(() => { + // Mark request as no longer pending + this.pushRoomStateTasks.delete(task) + }) + + // Mark task as pending + this.pushRoomStateTasks.add(task) + // Assuming no other tasks are already happening concurrently, + // schedule the widget action that actually pushes the events + this.flushRoomStateTask ??= this.flushRoomState() + this.flushRoomStateTask.catch((e) => + console.error("Failed to push room state", e), + ) + } + } } - } - /** + /** * Feeds a room state update to the widget. As a client you are expected to * call this for every state update in every room to which you are joined or * invited. @@ -1470,43 +1566,46 @@ export class ClientWidgetApi extends EventEmitter { * able to receive the room state due to permissions, rejects if the widget failed to handle the update. */ - public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) throw new Error("Not a state event"); - if ( - (rawEvent.room_id === this.viewedRoomId || - this.canUseRoomTimeline(rawEvent.room_id)) && - this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) - ) { - // Updates could race with the initial push of the room's state - if (this.pushRoomStateTasks.size === 0) { - // No initial push tasks are pending; safe to send immediately - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { - state: [rawEvent], - }, - ); - } else { - // Lump the update in with whatever data will be sent in the - // initial push later. Even if we set it to an "outdated" entry - // here, we can count on any newer entries being passed to this - // same method eventually; this won't cause stuck state. - let eventTypeMap = this.pushRoomStateResult.get(rawEvent.room_id); - if (eventTypeMap === undefined) { - eventTypeMap = new Map(); - this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap); - } - let stateKeyMap = eventTypeMap.get(rawEvent.type); - if (stateKeyMap === undefined) { - stateKeyMap = new Map(); - eventTypeMap.set(rawEvent.type, stateKeyMap); + public async feedStateUpdate(rawEvent: IRoomEvent): Promise { + if (rawEvent.state_key === undefined) + throw new Error("Not a state event") + if ( + (rawEvent.room_id === this.viewedRoomId || + this.canUseRoomTimeline(rawEvent.room_id)) && + this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) + ) { + // Updates could race with the initial push of the room's state + if (this.pushRoomStateTasks.size === 0) { + // No initial push tasks are pending; safe to send immediately + await this.transport.send( + WidgetApiToWidgetAction.UpdateState, + { + state: [rawEvent], + }, + ) + } else { + // Lump the update in with whatever data will be sent in the + // initial push later. Even if we set it to an "outdated" entry + // here, we can count on any newer entries being passed to this + // same method eventually; this won't cause stuck state. + let eventTypeMap = this.pushRoomStateResult.get( + rawEvent.room_id, + ) + if (eventTypeMap === undefined) { + eventTypeMap = new Map() + this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap) + } + let stateKeyMap = eventTypeMap.get(rawEvent.type) + if (stateKeyMap === undefined) { + stateKeyMap = new Map() + eventTypeMap.set(rawEvent.type, stateKeyMap) + } + if (!stateKeyMap.has(rawEvent.type)) + stateKeyMap.set(rawEvent.state_key, rawEvent) + do await Promise.all([...this.pushRoomStateTasks]) + while (this.pushRoomStateTasks.size > 0) + await this.flushRoomStateTask + } } - if (!stateKeyMap.has(rawEvent.type)) - stateKeyMap.set(rawEvent.state_key, rawEvent); - do await Promise.all([...this.pushRoomStateTasks]); - while (this.pushRoomStateTasks.size > 0); - await this.flushRoomStateTask; - } } - } } diff --git a/src/Symbols.ts b/src/Symbols.ts index 04ee9d0..85ca12e 100644 --- a/src/Symbols.ts +++ b/src/Symbols.ts @@ -15,5 +15,5 @@ */ export enum Symbols { - AnyRoom = "*", + AnyRoom = "*", } diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index 4793bc6..e08204c 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -14,127 +14,127 @@ * limitations under the License. */ -import { EventEmitter } from "events"; +import { EventEmitter } from "events" -import { Capability } from "./interfaces/Capabilities"; +import { Capability } from "./interfaces/Capabilities" import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./interfaces/IWidgetApiRequest"; -import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse"; -import { WidgetApiDirection } from "./interfaces/WidgetApiDirection"; + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./interfaces/IWidgetApiRequest" +import { IWidgetApiAcknowledgeResponseData } from "./interfaces/IWidgetApiResponse" +import { WidgetApiDirection } from "./interfaces/WidgetApiDirection" import { - ISupportedVersionsActionRequest, - ISupportedVersionsActionResponseData, -} from "./interfaces/SupportedVersionsAction"; + ISupportedVersionsActionRequest, + ISupportedVersionsActionResponseData, +} from "./interfaces/SupportedVersionsAction" import { - ApiVersion, - CurrentApiVersions, - UnstableApiVersion, -} from "./interfaces/ApiVersion"; + ApiVersion, + CurrentApiVersions, + UnstableApiVersion, +} from "./interfaces/ApiVersion" import { - ICapabilitiesActionRequest, - ICapabilitiesActionResponseData, - INotifyCapabilitiesActionRequest, - IRenegotiateCapabilitiesRequestData, -} from "./interfaces/CapabilitiesAction"; -import { ITransport } from "./transport/ITransport"; -import { PostmessageTransport } from "./transport/PostmessageTransport"; + ICapabilitiesActionRequest, + ICapabilitiesActionResponseData, + INotifyCapabilitiesActionRequest, + IRenegotiateCapabilitiesRequestData, +} from "./interfaces/CapabilitiesAction" +import { ITransport } from "./transport/ITransport" +import { PostmessageTransport } from "./transport/PostmessageTransport" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./interfaces/WidgetApiAction"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./interfaces/WidgetApiAction" import { - IWidgetApiErrorResponseData, - IWidgetApiErrorResponseDataDetails, -} from "./interfaces/IWidgetApiErrorResponse"; -import { IStickerActionRequestData } from "./interfaces/StickerAction"; + IWidgetApiErrorResponseData, + IWidgetApiErrorResponseDataDetails, +} from "./interfaces/IWidgetApiErrorResponse" +import { IStickerActionRequestData } from "./interfaces/StickerAction" import { - IStickyActionRequestData, - IStickyActionResponseData, -} from "./interfaces/StickyAction"; + IStickyActionRequestData, + IStickyActionResponseData, +} from "./interfaces/StickyAction" import { - IGetOpenIDActionRequestData, - IGetOpenIDActionResponse, - IOpenIDCredentials, - OpenIDRequestState, -} from "./interfaces/GetOpenIDAction"; -import { IOpenIDCredentialsActionRequest } from "./interfaces/OpenIDCredentialsAction"; -import { MatrixWidgetType, WidgetType } from "./interfaces/WidgetType"; + 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"; + BuiltInModalButtonID, + IModalWidgetCreateData, + IModalWidgetOpenRequestData, + IModalWidgetOpenRequestDataButton, + IModalWidgetReturnData, + ModalButtonID, +} from "./interfaces/ModalWidgetActions" +import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalButtonEnabledAction" import { - ISendEventFromWidgetRequestData, - ISendEventFromWidgetResponseData, -} from "./interfaces/SendEventAction"; + ISendEventFromWidgetRequestData, + ISendEventFromWidgetResponseData, +} from "./interfaces/SendEventAction" import { - ISendToDeviceFromWidgetRequestData, - ISendToDeviceFromWidgetResponseData, -} from "./interfaces/SendToDeviceAction"; + ISendToDeviceFromWidgetRequestData, + ISendToDeviceFromWidgetResponseData, +} from "./interfaces/SendToDeviceAction" import { - EventDirection, - WidgetEventCapability, -} from "./models/WidgetEventCapability"; -import { INavigateActionRequestData } from "./interfaces/NavigateAction"; + EventDirection, + WidgetEventCapability, +} from "./models/WidgetEventCapability" +import { INavigateActionRequestData } from "./interfaces/NavigateAction" import { - IReadEventFromWidgetRequestData, - IReadEventFromWidgetResponseData, -} from "./interfaces/ReadEventAction"; + IReadEventFromWidgetRequestData, + IReadEventFromWidgetResponseData, +} from "./interfaces/ReadEventAction" import { - IReadRoomAccountDataFromWidgetRequestData, - IReadRoomAccountDataFromWidgetResponseData, -} from "./interfaces/ReadRoomAccountDataAction"; -import { IRoomEvent } from "./interfaces/IRoomEvent"; -import { IRoomAccountData } from "./interfaces/IRoomAccountData"; + IReadRoomAccountDataFromWidgetRequestData, + IReadRoomAccountDataFromWidgetResponseData, +} from "./interfaces/ReadRoomAccountDataAction" +import { IRoomEvent } from "./interfaces/IRoomEvent" +import { IRoomAccountData } from "./interfaces/IRoomAccountData" import { - ITurnServer, - IUpdateTurnServersRequest, -} from "./interfaces/TurnServerActions"; -import { Symbols } from "./Symbols"; + ITurnServer, + IUpdateTurnServersRequest, +} from "./interfaces/TurnServerActions" +import { Symbols } from "./Symbols" import { - IReadRelationsFromWidgetRequestData, - IReadRelationsFromWidgetResponseData, -} from "./interfaces/ReadRelationsAction"; + IReadRelationsFromWidgetRequestData, + IReadRelationsFromWidgetResponseData, +} from "./interfaces/ReadRelationsAction" import { - IUserDirectorySearchFromWidgetRequestData, - IUserDirectorySearchFromWidgetResponseData, -} from "./interfaces/UserDirectorySearchAction"; + IUserDirectorySearchFromWidgetRequestData, + IUserDirectorySearchFromWidgetResponseData, +} from "./interfaces/UserDirectorySearchAction" import { - IGetMediaConfigActionFromWidgetRequestData, - IGetMediaConfigActionFromWidgetResponseData, -} from "./interfaces/GetMediaConfigAction"; + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData, +} from "./interfaces/GetMediaConfigAction" import { - IUploadFileActionFromWidgetRequestData, - IUploadFileActionFromWidgetResponseData, -} from "./interfaces/UploadFileAction"; + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData, +} from "./interfaces/UploadFileAction" import { - IDownloadFileActionFromWidgetRequestData, - IDownloadFileActionFromWidgetResponseData, -} from "./interfaces/DownloadFileAction"; + IDownloadFileActionFromWidgetRequestData, + IDownloadFileActionFromWidgetResponseData, +} from "./interfaces/DownloadFileAction" import { - IUpdateDelayedEventFromWidgetRequestData, - IUpdateDelayedEventFromWidgetResponseData, - UpdateDelayedEventAction, -} from "./interfaces/UpdateDelayedEventAction"; + IUpdateDelayedEventFromWidgetRequestData, + IUpdateDelayedEventFromWidgetResponseData, + UpdateDelayedEventAction, +} from "./interfaces/UpdateDelayedEventAction" export class WidgetApiResponseError extends Error { - static { - this.prototype.name = this.name; - } - - public constructor( - message: string, - public readonly data: IWidgetApiErrorResponseDataDetails, - ) { - super(message); - } + static { + this.prototype.name = this.name + } + + public constructor( + message: string, + public readonly data: IWidgetApiErrorResponseDataDetails, + ) { + super(message) + } } /** @@ -155,930 +155,989 @@ export class WidgetApiResponseError extends Error { * 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.", - ); + 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) } - 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); + + /** + * 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): void { + if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) { + throw new Error("Capabilities have already been negotiated") + } + + this.requestedCapabilities.push(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): void { - if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) { - throw new Error("Capabilities have already been negotiated"); + + /** + * 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[]): void { + 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, + ): void { + 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, + ): void { + 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, + ): void { + 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): void { + this.requestCapability( + WidgetEventCapability.forToDeviceEvent( + EventDirection.Send, + eventType, + ).raw, + ) } - 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[]): void { - 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, - ): void { - 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, - ): void { - 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, - ): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - 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): void { - this.requestCapability( - WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype) - .raw, - ); - } - - /** - * Requests the capability to receive a given item in room account data. 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. - */ - public requestCapabilityToReceiveRoomAccountData(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forRoomAccountData( - EventDirection.Receive, - eventType, - ).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} Resolves to a token for verification. - * @throws Throws if the user rejected the request or the request failed. - */ - public requestOpenIDConnectToken(): Promise { - return new Promise((resolve, reject) => { - this.transport - .sendComplete( - WidgetApiFromWidgetAction.GetOpenIDCredentials, - {}, + /** + * 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): void { + this.requestCapability( + WidgetEventCapability.forToDeviceEvent( + EventDirection.Receive, + eventType, + ).raw, ) - .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, - ): void => { - 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, {}); // ack - } else if (request.data.state === OpenIDRequestState.Blocked) { - reject(new Error("User declined to verify their identity")); - this.transport.reply(request, {}); // ack - } else { - reject(new Error("Invalid state on reply: " + rdata.state)); - this.transport.reply(request, { - error: { - message: "Invalid state", - }, - }); - } - this.off( - `action:${WidgetApiToWidgetAction.OpenIDCredentials}`, - handlerFn, - ); - }; - this.on( - `action:${WidgetApiToWidgetAction.OpenIDCredentials}`, - handlerFn, - ); - } else { - reject(new Error("Invalid state: " + rdata.state)); - } + } + + /** + * 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): void { + 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): void { + 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): void { + 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): void { + this.requestCapability( + WidgetEventCapability.forRoomMessageEvent( + EventDirection.Receive, + msgtype, + ).raw, + ) + } + + /** + * Requests the capability to receive a given item in room account data. 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. + */ + public requestCapabilityToReceiveRoomAccountData(eventType: string): void { + this.requestCapability( + WidgetEventCapability.forRoomAccountData( + EventDirection.Receive, + eventType, + ).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} Resolves to a token for verification. + * @throws Throws if the user rejected the request or the request failed. + */ + public requestOpenIDConnectToken(): Promise { + return new Promise((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, + ): void => { + 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, + {}, + ) // ack + } else if ( + request.data.state === + OpenIDRequestState.Blocked + ) { + reject( + new Error( + "User declined to verify their identity", + ), + ) + this.transport.reply( + request, + {}, + ) // 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) }) - .catch(reject); - }); - } - - /** - * Asks the client for additional capabilities. Capabilities can be queued for this - * request with the requestCapability() functions. - * @returns {Promise} 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 { - 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 { - return this.transport - .send( - WidgetApiFromWidgetAction.ContentLoaded, - {}, - ) - .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 { - 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} 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 { - 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} Resolves when the modal widget has been opened. - */ - public openModalWidget( - url: string, - name: string, - buttons: IModalWidgetOpenRequestDataButton[] = [], - data: IModalWidgetCreateData = {}, - type: WidgetType = MatrixWidgetType.Custom, - ): Promise { - return this.transport - .send( - 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} Resolves when complete. - */ - public closeModalWidget(data: IModalWidgetReturnData = {}): Promise { - return this.transport - .send( - WidgetApiFromWidgetAction.CloseModalWidget, - data, - ) - .then(); - } - - public sendRoomEvent( - eventType: string, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.sendEvent( - eventType, - undefined, - content, - roomId, - delay, - parentDelayId, - ); - } - - public sendStateEvent( - eventType: string, - stateKey: string, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.sendEvent( - eventType, - stateKey, - content, - roomId, - delay, - parentDelayId, - ); - } - - private sendEvent( - eventType: string, - stateKey: string | undefined, - content: unknown, - roomId?: string, - delay?: number, - parentDelayId?: string, - ): Promise { - return this.transport.send< - ISendEventFromWidgetRequestData, - ISendEventFromWidgetResponseData - >(WidgetApiFromWidgetAction.SendEvent, { - type: eventType, - content, - ...(stateKey !== undefined && { state_key: stateKey }), - ...(roomId !== undefined && { room_id: roomId }), - ...(delay !== undefined && { delay }), - ...(parentDelayId !== undefined && { parent_delay_id: parentDelayId }), - }); - } - - /** - * @deprecated This currently relies on an unstable MSC (MSC4157). - */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { - return this.transport.send< - IUpdateDelayedEventFromWidgetRequestData, - IUpdateDelayedEventFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, { - delay_id: delayId, - action, - }); - } - - /** - * 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} Resolves when complete. - */ - public sendToDevice( - eventType: string, - encrypted: boolean, - contentMap: { [userId: string]: { [deviceId: string]: object } }, - ): Promise { - return this.transport.send< - ISendToDeviceFromWidgetRequestData, - ISendToDeviceFromWidgetResponseData - >(WidgetApiFromWidgetAction.SendToDevice, { - type: eventType, - encrypted, - messages: contentMap, - }); - } - - public readRoomAccountData( - eventType: string, - roomIds?: (string | Symbols.AnyRoom)[], - ): Promise { - const data: IReadEventFromWidgetRequestData = { type: eventType }; - - if (roomIds) { - if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom; - } else { - data.room_ids = roomIds; - } } - return this.transport - .send< - IReadRoomAccountDataFromWidgetRequestData, - IReadRoomAccountDataFromWidgetResponseData - >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) - .then((r) => r.events); - } - - public readRoomEvents( - eventType: string, - limit?: number, - msgtype?: string, - roomIds?: (string | Symbols.AnyRoom)[], - since?: string | undefined, - ): Promise { - const data: IReadEventFromWidgetRequestData = { - type: eventType, - msgtype: msgtype, - }; - if (limit !== undefined) { - data.limit = limit; + + /** + * Asks the client for additional capabilities. Capabilities can be queued for this + * request with the requestCapability() functions. + * @returns {Promise} 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 { + 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 { + return this.transport + .send( + WidgetApiFromWidgetAction.ContentLoaded, + {}, + ) + .then() } - if (roomIds) { - if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom; - } else { - data.room_ids = roomIds; - } + + /** + * 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 { + return this.transport + .send(WidgetApiFromWidgetAction.SendSticker, sticker) + .then() } - if (since) { - data.since = since; + + /** + * Asks the client to set the always-on-screen status for this widget. + * @param {boolean} value The new state to request. + * @returns {Promise} 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 { + return this.transport + .send< + IStickyActionRequestData, + IStickyActionResponseData + >(WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, { value }) + .then((res) => res.success) } - 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 { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC3869)) { - throw new Error( - "The read_relations action is not supported by the client.", - ); + + /** + * 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} Resolves when the modal widget has been opened. + */ + public openModalWidget( + url: string, + name: string, + buttons: IModalWidgetOpenRequestDataButton[] = [], + data: IModalWidgetCreateData = {}, + type: WidgetType = MatrixWidgetType.Custom, + ): Promise { + return this.transport + .send( + WidgetApiFromWidgetAction.OpenModalWidget, + { + type, + url, + name, + buttons, + data, + }, + ) + .then() } - 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 { - const data: IReadEventFromWidgetRequestData = { - type: eventType, - state_key: stateKey === undefined ? true : stateKey, - }; - if (limit !== undefined) { - data.limit = limit; + /** + * 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} Resolves when complete. + */ + public closeModalWidget(data: IModalWidgetReturnData = {}): Promise { + return this.transport + .send( + WidgetApiFromWidgetAction.CloseModalWidget, + data, + ) + .then() } - if (roomIds) { - if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom; - } else { - data.room_ids = roomIds; - } + + public sendRoomEvent( + eventType: string, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.sendEvent( + eventType, + undefined, + content, + roomId, + delay, + parentDelayId, + ) } - 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} 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 { - if (buttonId === BuiltInModalButtonID.Close) { - throw new Error("The close button cannot be disabled"); + + public sendStateEvent( + eventType: string, + stateKey: string, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.sendEvent( + eventType, + stateKey, + content, + roomId, + delay, + parentDelayId, + ) } - return this.transport - .send( - 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} 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 { - if (!uri || !uri.startsWith("https://matrix.to/#")) { - throw new Error("Invalid matrix.to URI"); + + private sendEvent( + eventType: string, + stateKey: string | undefined, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ): Promise { + return this.transport.send< + ISendEventFromWidgetRequestData, + ISendEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.SendEvent, { + type: eventType, + content, + ...(stateKey !== undefined && { state_key: stateKey }), + ...(roomId !== undefined && { room_id: roomId }), + ...(delay !== undefined && { delay }), + ...(parentDelayId !== undefined && { + parent_delay_id: parentDelayId, + }), + }) } - return this.transport - .send( - 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 { - let setTurnServer: (server: ITurnServer) => void; - - const onUpdateTurnServers = async ( - ev: CustomEvent, - ): Promise => { - ev.preventDefault(); - setTurnServer(ev.detail.data); - await this.transport.reply( - 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( - WidgetApiFromWidgetAction.WatchTurnServers, - {}, - ); - } catch (e) { - this.off( - `action:${WidgetApiToWidgetAction.UpdateTurnServers}`, - onUpdateTurnServers, - ); - throw e; - } + /** + * @deprecated This currently relies on an unstable MSC (MSC4157). + */ + public updateDelayedEvent( + delayId: string, + action: UpdateDelayedEventAction, + ): Promise { + return this.transport.send< + IUpdateDelayedEventFromWidgetRequestData, + IUpdateDelayedEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, { + delay_id: delayId, + action, + }) } - this.turnServerWatchers++; - - try { - // Watch for new data indefinitely (until this generator's return method is called) - while (true) { - yield await new Promise( - (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( - WidgetApiFromWidgetAction.UnwatchTurnServers, - {}, - ); - } + + /** + * 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} Resolves when complete. + */ + public sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + return this.transport.send< + ISendToDeviceFromWidgetRequestData, + ISendToDeviceFromWidgetResponseData + >(WidgetApiFromWidgetAction.SendToDevice, { + type: eventType, + encrypted, + messages: contentMap, + }) } - } - - /** - * 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 { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC3973)) { - throw new Error( - "The user_directory_search action is not supported by the client.", - ); + + public readRoomAccountData( + eventType: string, + roomIds?: (string | Symbols.AnyRoom)[], + ): Promise { + const data: IReadEventFromWidgetRequestData = { type: eventType } + + if (roomIds) { + if (roomIds.includes(Symbols.AnyRoom)) { + data.room_ids = Symbols.AnyRoom + } else { + data.room_ids = roomIds + } + } + return this.transport + .send< + IReadRoomAccountDataFromWidgetRequestData, + IReadRoomAccountDataFromWidgetResponseData + >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) + .then((r) => r.events) } - const data: IUserDirectorySearchFromWidgetRequestData = { - search_term: searchTerm, - limit, - }; - - return this.transport.send< - IUserDirectorySearchFromWidgetRequestData, - IUserDirectorySearchFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data); - } - - /** - * Get the config for the media repository. - * @returns Promise which resolves with an object containing the config. - */ - public async getMediaConfig(): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error( - "The get_media_config action is not supported by the client.", - ); + public readRoomEvents( + eventType: string, + limit?: number, + msgtype?: string, + roomIds?: (string | Symbols.AnyRoom)[], + since?: string | undefined, + ): Promise { + 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 + } + } + if (since) { + data.since = since + } + return this.transport + .send< + IReadEventFromWidgetRequestData, + IReadEventFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC2876ReadEvents, data) + .then((r) => r.events) } - const data: IGetMediaConfigActionFromWidgetRequestData = {}; - - return this.transport.send< - IGetMediaConfigActionFromWidgetRequestData, - IGetMediaConfigActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data); - } - - /** - * Upload a file to the media repository on the homeserver. - * @param file - The object to upload. Something that can be sent to - * XMLHttpRequest.send (typically a File). - * @returns Resolves to the location of the uploaded file. - */ - public async uploadFile( - file: XMLHttpRequestBodyInit, - ): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error("The upload_file action is not supported by the client."); + /** + * 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 { + 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) } - const data: IUploadFileActionFromWidgetRequestData = { - file, - }; - - return this.transport.send< - IUploadFileActionFromWidgetRequestData, - IUploadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data); - } - - /** - * Download a file from the media repository on the homeserver. - * @param contentUri - MXC URI of the file to download. - * @returns Resolves to the contents of the file. - */ - public async downloadFile( - contentUri: string, - ): Promise { - const versions = await this.getClientVersions(); - if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error( - "The download_file action is not supported by the client.", - ); + public readStateEvents( + eventType: string, + limit?: number, + stateKey?: string, + roomIds?: (string | Symbols.AnyRoom)[], + ): Promise { + 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) } - const data: IDownloadFileActionFromWidgetRequestData = { - content_uri: contentUri, - }; - - return this.transport.send< - IDownloadFileActionFromWidgetRequestData, - IDownloadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039DownloadFileAction, 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(): void { - this.transport.start(); - this.getClientVersions().then((v) => { - if (v.includes(UnstableApiVersion.MSC2974)) { - this.supportsMSC2974Renegotiate = true; - } - }); - } - - private handleMessage( - ev: CustomEvent, - ): void | Promise { - 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(ev.detail); - case WidgetApiToWidgetAction.Capabilities: - return this.handleCapabilities(ev.detail); - case WidgetApiToWidgetAction.UpdateVisibility: - return this.transport.reply( - ev.detail, - {}, - ); // ack to avoid error spam - case WidgetApiToWidgetAction.NotifyCapabilities: - return this.transport.reply( - ev.detail, - {}, - ); // ack to avoid error spam - default: - return this.transport.reply(ev.detail, { - error: { - message: "Unknown or unsupported action: " + ev.detail.action, - }, - }); - } + /** + * 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} 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 { + if (buttonId === BuiltInModalButtonID.Close) { + throw new Error("The close button cannot be disabled") + } + return this.transport + .send( + WidgetApiFromWidgetAction.SetModalButtonEnabled, + { + button: buttonId, + enabled: isEnabled, + }, + ) + .then() } - } - private replyVersions(request: ISupportedVersionsActionRequest): void { - this.transport.reply(request, { - supported_versions: CurrentApiVersions, - }); - } + /** + * 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} 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 { + if (!uri || !uri.startsWith("https://matrix.to/#")) { + throw new Error("Invalid matrix.to URI") + } + + return this.transport + .send( + 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 { + let setTurnServer: (server: ITurnServer) => void + + const onUpdateTurnServers = async ( + ev: CustomEvent, + ): Promise => { + ev.preventDefault() + setTurnServer(ev.detail.data) + await this.transport.reply( + 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, + ) - public getClientVersions(): Promise { - if (Array.isArray(this.cachedClientVersions)) { - return Promise.resolve(this.cachedClientVersions); + // Only send the 'watch' action if we aren't already watching + if (this.turnServerWatchers === 0) { + try { + await this.transport.send( + 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( + (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( + WidgetApiFromWidgetAction.UnwatchTurnServers, + {}, + ) + } + } } - return this.transport - .send( - 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, - ): void | Promise { - if (this.capabilitiesFinished) { - return this.transport.reply(request, { - error: { - message: "Capability negotiation already completed", - }, - }); + /** + * 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 { + 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) } - // 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) => { - 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(request, { - capabilities: this.requestedCapabilities, - }); - }); - } + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public async getMediaConfig(): Promise { + const versions = await this.getClientVersions() + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error( + "The get_media_config action is not supported by the client.", + ) + } + + const data: IGetMediaConfigActionFromWidgetRequestData = {} + + return this.transport.send< + IGetMediaConfigActionFromWidgetRequestData, + IGetMediaConfigActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data) + } + + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public async uploadFile( + file: XMLHttpRequestBodyInit, + ): Promise { + const versions = await this.getClientVersions() + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error( + "The upload_file action is not supported by the client.", + ) + } + + const data: IUploadFileActionFromWidgetRequestData = { + file, + } + + return this.transport.send< + IUploadFileActionFromWidgetRequestData, + IUploadFileActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data) + } + + /** + * Download a file from the media repository on the homeserver. + * @param contentUri - MXC URI of the file to download. + * @returns Resolves to the contents of the file. + */ + public async downloadFile( + contentUri: string, + ): Promise { + const versions = await this.getClientVersions() + if (!versions.includes(UnstableApiVersion.MSC4039)) { + throw new Error( + "The download_file action is not supported by the client.", + ) + } + + const data: IDownloadFileActionFromWidgetRequestData = { + content_uri: contentUri, + } + + return this.transport.send< + IDownloadFileActionFromWidgetRequestData, + IDownloadFileActionFromWidgetResponseData + >(WidgetApiFromWidgetAction.MSC4039DownloadFileAction, 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(): void { + this.transport.start() + this.getClientVersions().then((v) => { + if (v.includes(UnstableApiVersion.MSC2974)) { + this.supportsMSC2974Renegotiate = true + } + }) + } + + private handleMessage( + ev: CustomEvent, + ): void | Promise { + 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( + ev.detail, + ) + case WidgetApiToWidgetAction.Capabilities: + return this.handleCapabilities( + ev.detail, + ) + case WidgetApiToWidgetAction.UpdateVisibility: + return this.transport.reply( + ev.detail, + {}, + ) // ack to avoid error spam + case WidgetApiToWidgetAction.NotifyCapabilities: + return this.transport.reply( + ev.detail, + {}, + ) // 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): void { + this.transport.reply(request, { + supported_versions: CurrentApiVersions, + }) + } + + public getClientVersions(): Promise { + 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, + ): void | Promise { + if (this.capabilitiesFinished) { + return this.transport.reply(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) => { + 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( + request, + { + capabilities: this.requestedCapabilities, + }, + ) + }) + } } diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index c69767e..20fa098 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -15,50 +15,50 @@ */ import { - Capability, - IOpenIDCredentials, - OpenIDRequestState, - SimpleObservable, - IRoomEvent, - IRoomAccountData, - ITurnServer, - IWidgetApiErrorResponseDataDetails, - UpdateDelayedEventAction, -} from ".."; + Capability, + IOpenIDCredentials, + OpenIDRequestState, + SimpleObservable, + IRoomEvent, + IRoomAccountData, + ITurnServer, + IWidgetApiErrorResponseDataDetails, + UpdateDelayedEventAction, +} from ".." export interface ISendEventDetails { - roomId: string; - eventId: string; + roomId: string + eventId: string } export interface ISendDelayedEventDetails { - roomId: string; - delayId: string; + roomId: string + delayId: string } export interface IOpenIDUpdate { - state: OpenIDRequestState; - token?: IOpenIDCredentials; + state: OpenIDRequestState + token?: IOpenIDCredentials } export interface IReadEventRelationsResult { - chunk: IRoomEvent[]; - nextBatch?: string; - prevBatch?: string; + chunk: IRoomEvent[] + nextBatch?: string + prevBatch?: string } export interface ISearchUserDirectoryResult { - limited: boolean; - results: Array<{ - userId: string; - displayName?: string; - avatarUrl?: string; - }>; + limited: boolean + results: Array<{ + userId: string + displayName?: string + avatarUrl?: string + }> } export interface IGetMediaConfigResult { - [key: string]: unknown; - "m.upload.size"?: number; + [key: string]: unknown + "m.upload.size"?: number } /** @@ -71,366 +71,372 @@ export interface IGetMediaConfigResult { * instance already. */ export abstract class WidgetDriver { - /** - * Verifies the widget's requested capabilities, returning the ones - * it is approved to use. Mutating the requested capabilities will - * have no effect. - * - * This SHOULD result in the user being prompted to approve/deny - * capabilities. - * - * By default this rejects all capabilities (returns an empty set). - * @param {Set} requested The set of requested capabilities. - * @returns {Promise>} Resolves to the allowed capabilities. - */ - public validateCapabilities( - requested: Set, - ): Promise> { - return Promise.resolve(new Set()); - } + /** + * Verifies the widget's requested capabilities, returning the ones + * it is approved to use. Mutating the requested capabilities will + * have no effect. + * + * This SHOULD result in the user being prompted to approve/deny + * capabilities. + * + * By default this rejects all capabilities (returns an empty set). + * @param {Set} requested The set of requested capabilities. + * @returns {Promise>} Resolves to the allowed capabilities. + */ + public validateCapabilities( + requested: Set, + ): Promise> { + return Promise.resolve(new Set()) + } - /** - * Sends an event into a room. If `roomId` is falsy, the client should send the event - * into the room the user is currently looking at. The widget API will have already - * verified that the widget is capable of sending the event to that room. - * @param {string} eventType The event type to be sent. - * @param {*} content The content for the event. - * @param {string|null} stateKey The state key if this is a state event, otherwise null. - * May be an empty string. - * @param {string|null} roomId The room ID to send the event to. If falsy, the room the - * user is currently looking at. - * @returns {Promise} Resolves when the event has been sent with - * details of that event. - * @throws Rejected when the event could not be sent. - */ - public sendEvent( - eventType: string, - content: unknown, - stateKey: string | null = null, - roomId: string | null = null, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * Sends an event into a room. If `roomId` is falsy, the client should send the event + * into the room the user is currently looking at. The widget API will have already + * verified that the widget is capable of sending the event to that room. + * @param {string} eventType The event type to be sent. + * @param {*} content The content for the event. + * @param {string|null} stateKey The state key if this is a state event, otherwise null. + * May be an empty string. + * @param {string|null} roomId The room ID to send the event to. If falsy, the room the + * user is currently looking at. + * @returns {Promise} Resolves when the event has been sent with + * details of that event. + * @throws Rejected when the event could not be sent. + */ + public sendEvent( + eventType: string, + content: unknown, + stateKey: string | null = null, + roomId: string | null = null, + ): Promise { + return Promise.reject(new Error("Failed to override function")) + } - /** - * @experimental Part of MSC4140 & MSC4157 - * Sends a delayed event into a room. If `roomId` is falsy, the client should send it - * into the room the user is currently looking at. The widget API will have already - * verified that the widget is capable of sending the event to that room. - * @param {number|null} delay How much later to send the event, or null to not send the - * event automatically. May not be null if {@link parentDelayId} is null. - * @param {string|null} parentDelayId The ID of the delayed event this one is grouped with, - * or null if it will be put in a new group. May not be null if {@link delay} is null. - * @param {string} eventType The event type of the event to be sent. - * @param {*} content The content for the event to be sent. - * @param {string|null} stateKey The state key if the event to be sent a state event, - * otherwise null. May be an empty string. - * @param {string|null} roomId The room ID to send the event to. If falsy, the room the - * user is currently looking at. - * @returns {Promise} Resolves when the delayed event has been - * prepared with details of how to refer to it for updating/sending/canceling it later. - * @throws Rejected when the delayed event could not be sent. - */ - public sendDelayedEvent( - delay: number | null, - parentDelayId: string | null, - eventType: string, - content: unknown, - stateKey: string | null = null, - roomId: string | null = null, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * @experimental Part of MSC4140 & MSC4157 + * Sends a delayed event into a room. If `roomId` is falsy, the client should send it + * into the room the user is currently looking at. The widget API will have already + * verified that the widget is capable of sending the event to that room. + * @param {number|null} delay How much later to send the event, or null to not send the + * event automatically. May not be null if {@link parentDelayId} is null. + * @param {string|null} parentDelayId The ID of the delayed event this one is grouped with, + * or null if it will be put in a new group. May not be null if {@link delay} is null. + * @param {string} eventType The event type of the event to be sent. + * @param {*} content The content for the event to be sent. + * @param {string|null} stateKey The state key if the event to be sent a state event, + * otherwise null. May be an empty string. + * @param {string|null} roomId The room ID to send the event to. If falsy, the room the + * user is currently looking at. + * @returns {Promise} Resolves when the delayed event has been + * prepared with details of how to refer to it for updating/sending/canceling it later. + * @throws Rejected when the delayed event could not be sent. + */ + public sendDelayedEvent( + delay: number | null, + parentDelayId: string | null, + eventType: string, + content: unknown, + stateKey: string | null = null, + roomId: string | null = null, + ): Promise { + return Promise.reject(new Error("Failed to override function")) + } - /** - * @experimental Part of MSC4140 & MSC4157 - * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. - * @throws Rejected when there is no matching delayed event, or when the action failed to run. - */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + /** + * @experimental Part of MSC4140 & MSC4157 + * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. + * @throws Rejected when there is no matching delayed event, or when the action failed to run. + */ + public updateDelayedEvent( + delayId: string, + action: UpdateDelayedEventAction, + ): Promise { + return Promise.reject(new Error("Failed to override function")) + } - /** - * Sends a to-device event. The widget API will have already verified that the widget - * is capable of sending the event. - * @param {string} eventType The event type to be sent. - * @param {boolean} encrypted Whether to encrypt the message contents. - * @param {Object} contentMap A map from user ID and device ID to event content. - * @returns {Promise} Resolves when the event has been sent. - * @throws Rejected when the event could not be sent. - */ - public sendToDevice( - eventType: string, - encrypted: boolean, - contentMap: { [userId: string]: { [deviceId: string]: object } }, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } - /** - * Reads an element of room account data. The widget API will have already verified that the widget is - * capable of receiving the `eventType` of the requested information. If `roomIds` is supplied, it may - * contain `Symbols.AnyRoom` to denote that the piece of room account data in each of the client's known - * rooms should be returned. When `null`, only the room the user is currently looking at should be considered. - * @param eventType The event type to be read. - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @returns {Promise} Resolves to the element of room account data, or an empty array. - */ - public readRoomAccountData( - eventType: string, - roomIds: string[] | null = null, - ): Promise { - return Promise.resolve([]); - } + /** + * Sends a to-device event. The widget API will have already verified that the widget + * is capable of sending the event. + * @param {string} eventType The event type to be sent. + * @param {boolean} encrypted Whether to encrypt the message contents. + * @param {Object} contentMap A map from user ID and device ID to event content. + * @returns {Promise} Resolves when the event has been sent. + * @throws Rejected when the event could not be sent. + */ + public sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + return Promise.reject(new Error("Failed to override function")) + } + /** + * Reads an element of room account data. The widget API will have already verified that the widget is + * capable of receiving the `eventType` of the requested information. If `roomIds` is supplied, it may + * contain `Symbols.AnyRoom` to denote that the piece of room account data in each of the client's known + * rooms should be returned. When `null`, only the room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the element of room account data, or an empty array. + */ + public readRoomAccountData( + eventType: string, + roomIds: string[] | null = null, + ): Promise { + return Promise.resolve([]) + } - /** - * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that - * `limit` in each of the client's known rooms should be returned. When `null`, only the - * room the user is currently looking at should be considered. If `since` is specified but - * the event ID isn't present in the number of events fetched by the client due to `limit`, - * the client will return all the events. - * @param eventType The event type to be read. - * @param msgtype The msgtype of the events to be read, if applicable/defined. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many - * as possible". - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @param since When null, retrieves the number of events specified by the "limit" parameter. - * Otherwise, the event ID at which only subsequent events will be returned, as many as specified - * in "limit". - * @returns {Promise} Resolves to the room events, or an empty array. - * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. - */ - public readRoomEvents( - eventType: string, - msgtype: string | undefined, - limit: number, - roomIds: string[] | null = null, - since?: string, - ): Promise { - return Promise.resolve([]); - } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. If `since` is specified but + * the event ID isn't present in the number of events fetched by the client due to `limit`, + * the client will return all the events. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @param since When null, retrieves the number of events specified by the "limit" parameter. + * Otherwise, the event ID at which only subsequent events will be returned, as many as specified + * in "limit". + * @returns {Promise} Resolves to the room events, or an empty array. + * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. + */ + public readRoomEvents( + eventType: string, + msgtype: string | undefined, + limit: number, + roomIds: string[] | null = null, + since?: string, + ): Promise { + return Promise.resolve([]) + } - /** - * Reads all events of the given type, and optionally state key (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that - * `limit` in each of the client's known rooms should be returned. When `null`, only the - * room the user is currently looking at should be considered. - * @param eventType The event type to be read. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve. Will be zero to denote "as many - * as possible". - * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs - * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. - * @returns {Promise} Resolves to the state events, or an empty array. - * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. - */ - public readStateEvents( - eventType: string, - stateKey: string | undefined, - limit: number, - roomIds: string[] | null = null, - ): Promise { - return Promise.resolve([]); - } + /** + * Reads all events of the given type, and optionally state key (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the state events, or an empty array. + * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. + */ + public readStateEvents( + eventType: string, + stateKey: string | undefined, + limit: number, + roomIds: string[] | null = null, + ): Promise { + return Promise.resolve([]) + } - /** - * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), - * the user has access to. The widget API will have already verified that the widget is - * capable of receiving the events. Less events than the limit are allowed to be returned, - * but not more. - * @param roomId The ID of the room to look within. - * @param eventType The event type to be read. - * @param msgtype The msgtype of the events to be read, if applicable/defined. - * @param stateKey The state key of the events to be read, if applicable/defined. - * @param limit The maximum number of events to retrieve. Will be zero to denote "as many as - * possible". - * @param since When null, retrieves the number of events specified by the "limit" parameter. - * Otherwise, the event ID at which only subsequent events will be returned, as many as specified - * in "limit". - * @returns {Promise} Resolves to the room events, or an empty array. - */ - public readRoomTimeline( - roomId: string, - eventType: string, - msgtype: string | undefined, - stateKey: string | undefined, - limit: number, - since: string | undefined, - ): Promise { - // For backward compatibility we try the deprecated methods, in case - // they're implemented - if (stateKey === undefined) - return this.readRoomEvents(eventType, msgtype, limit, [roomId], since); - else return this.readStateEvents(eventType, stateKey, limit, [roomId]); - } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. + * @param roomId The ID of the room to look within. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many as + * possible". + * @param since When null, retrieves the number of events specified by the "limit" parameter. + * Otherwise, the event ID at which only subsequent events will be returned, as many as specified + * in "limit". + * @returns {Promise} Resolves to the room events, or an empty array. + */ + public readRoomTimeline( + roomId: string, + eventType: string, + msgtype: string | undefined, + stateKey: string | undefined, + limit: number, + since: string | undefined, + ): Promise { + // For backward compatibility we try the deprecated methods, in case + // they're implemented + if (stateKey === undefined) + return this.readRoomEvents( + eventType, + msgtype, + limit, + [roomId], + since, + ) + else return this.readStateEvents(eventType, stateKey, limit, [roomId]) + } - /** - * Reads the current values of all matching room state entries. - * @param roomId The ID of the room. - * @param eventType The event type of the entries to be read. - * @param stateKey The state key of the entry to be read. If undefined, - * all room state entries with a matching event type should be returned. - * @returns {Promise} Resolves to the events representing the - * current values of the room state entries. - */ - public readRoomState( - roomId: string, - eventType: string, - stateKey: string | undefined, - ): Promise { - return Promise.resolve([]); - } + /** + * Reads the current values of all matching room state entries. + * @param roomId The ID of the room. + * @param eventType The event type of the entries to be read. + * @param stateKey The state key of the entry to be read. If undefined, + * all room state entries with a matching event type should be returned. + * @returns {Promise} Resolves to the events representing the + * current values of the room state entries. + */ + public readRoomState( + roomId: string, + eventType: string, + stateKey: string | undefined, + ): Promise { + return Promise.resolve([]) + } - /** - * Reads all events that are related to a given event. The widget API will - * have already verified that the widget is capable of receiving the event, - * or will make sure to reject access to events which are returned from this - * function, but are not capable of receiving. If `relationType` or `eventType` - * are set, the returned events should already be filtered. Less events than - * the limit are allowed to be returned, but not more. - * @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 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 limit The maximum number of events to retrieve per room. If not - * supplied, the server will apply a default limit. - * @param direction The direction to search for according to MSC3715 - * @returns Resolves to the room relations. - */ - public readEventRelations( - eventId: string, - roomId?: string, - relationType?: string, - eventType?: string, - from?: string, - to?: string, - limit?: number, - direction?: "f" | "b", - ): Promise { - return Promise.resolve({ chunk: [] }); - } + /** + * Reads all events that are related to a given event. The widget API will + * have already verified that the widget is capable of receiving the event, + * or will make sure to reject access to events which are returned from this + * function, but are not capable of receiving. If `relationType` or `eventType` + * are set, the returned events should already be filtered. Less events than + * the limit are allowed to be returned, but not more. + * @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 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 limit The maximum number of events to retrieve per room. If not + * supplied, the server will apply a default limit. + * @param direction The direction to search for according to MSC3715 + * @returns Resolves to the room relations. + */ + public readEventRelations( + eventId: string, + roomId?: string, + relationType?: string, + eventType?: string, + from?: string, + to?: string, + limit?: number, + direction?: "f" | "b", + ): Promise { + return Promise.resolve({ chunk: [] }) + } - /** - * Asks the user for permission to validate their identity through OpenID Connect. The - * interface for this function is an observable which accepts the state machine of the - * OIDC exchange flow. For example, if the client/user blocks the request then it would - * feed back a `{state: Blocked}` into the observable. Similarly, if the user already - * approved the widget then a `{state: Allowed}` would be fed into the observable alongside - * the token itself. If the client is asking for permission, it should feed in a - * `{state: PendingUserConfirmation}` followed by the relevant Allowed or Blocked state. - * - * The widget API will reject the widget's request with an error if this contract is not - * met properly. By default, the widget driver will block all OIDC requests. - * @param {SimpleObservable} observer The observable to feed updates into. - */ - public askOpenID(observer: SimpleObservable): void { - observer.update({ state: OpenIDRequestState.Blocked }); - } + /** + * Asks the user for permission to validate their identity through OpenID Connect. The + * interface for this function is an observable which accepts the state machine of the + * OIDC exchange flow. For example, if the client/user blocks the request then it would + * feed back a `{state: Blocked}` into the observable. Similarly, if the user already + * approved the widget then a `{state: Allowed}` would be fed into the observable alongside + * the token itself. If the client is asking for permission, it should feed in a + * `{state: PendingUserConfirmation}` followed by the relevant Allowed or Blocked state. + * + * The widget API will reject the widget's request with an error if this contract is not + * met properly. By default, the widget driver will block all OIDC requests. + * @param {SimpleObservable} observer The observable to feed updates into. + */ + public askOpenID(observer: SimpleObservable): void { + observer.update({ state: OpenIDRequestState.Blocked }) + } - /** - * Navigates the client with a matrix.to URI. In future this function will also be provided - * with the Matrix URIs once matrix.to is replaced. The given URI will have already been - * lightly checked to ensure it looks like a valid URI, though the implementation is recommended - * to do further checks on the URI. - * @param {string} uri The URI to navigate to. - * @returns {Promise} Resolves when complete. - * @throws Throws if there's a problem with the navigation, such as invalid format. - */ - public navigate(uri: string): Promise { - throw new Error("Navigation is not implemented"); - } + /** + * Navigates the client with a matrix.to URI. In future this function will also be provided + * with the Matrix URIs once matrix.to is replaced. The given URI will have already been + * lightly checked to ensure it looks like a valid URI, though the implementation is recommended + * to do further checks on the URI. + * @param {string} uri The URI to navigate to. + * @returns {Promise} Resolves when complete. + * @throws Throws if there's a problem with the navigation, such as invalid format. + */ + public navigate(uri: string): Promise { + throw new Error("Navigation is not implemented") + } - /** - * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and - * thereafter yielding new credentials whenever the previous ones expire. The widget API will - * have already verified that the widget has permission to access TURN servers. - * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. - */ - public getTurnServers(): AsyncGenerator { - throw new Error("TURN server support is not implemented"); - } + /** + * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and + * thereafter yielding new credentials whenever the previous ones expire. The widget API will + * have already verified that the widget has permission to access TURN servers. + * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. + */ + public getTurnServers(): AsyncGenerator { + throw new Error("TURN server support is not implemented") + } - /** - * 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 searchUserDirectory( - searchTerm: string, - limit?: number, - ): Promise { - return Promise.resolve({ limited: false, results: [] }); - } + /** + * 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 searchUserDirectory( + searchTerm: string, + limit?: number, + ): Promise { + return Promise.resolve({ limited: false, results: [] }) + } - /** - * Get the config for the media repository. - * @returns Promise which resolves with an object containing the config. - */ - public getMediaConfig(): Promise { - throw new Error("Get media config is not implemented"); - } + /** + * Get the config for the media repository. + * @returns Promise which resolves with an object containing the config. + */ + public getMediaConfig(): Promise { + throw new Error("Get media config is not implemented") + } - /** - * Upload a file to the media repository on the homeserver. - * @param file - The object to upload. Something that can be sent to - * XMLHttpRequest.send (typically a File). - * @returns Resolves to the location of the uploaded file. - */ - public uploadFile( - file: XMLHttpRequestBodyInit, - ): Promise<{ contentUri: string }> { - throw new Error("Upload file is not implemented"); - } + /** + * Upload a file to the media repository on the homeserver. + * @param file - The object to upload. Something that can be sent to + * XMLHttpRequest.send (typically a File). + * @returns Resolves to the location of the uploaded file. + */ + public uploadFile( + file: XMLHttpRequestBodyInit, + ): Promise<{ contentUri: string }> { + throw new Error("Upload file is not implemented") + } - /** - * Download a file from the media repository on the homeserver. - * @param contentUri - MXC URI of the file to download. - * @returns Resolves to the contents of the file. - */ - public downloadFile( - contentUri: string, - ): Promise<{ file: XMLHttpRequestBodyInit }> { - throw new Error("Download file is not implemented"); - } + /** + * Download a file from the media repository on the homeserver. + * @param contentUri - MXC URI of the file to download. + * @returns Resolves to the contents of the file. + */ + public downloadFile( + contentUri: string, + ): Promise<{ file: XMLHttpRequestBodyInit }> { + throw new Error("Download file is not implemented") + } - /** - * Gets the IDs of all joined or invited rooms currently known to the - * client. - * @returns The room IDs. - */ - public getKnownRooms(): string[] { - throw new Error("Querying known rooms is not implemented"); - } + /** + * Gets the IDs of all joined or invited rooms currently known to the + * client. + * @returns The room IDs. + */ + public getKnownRooms(): string[] { + throw new Error("Querying known rooms is not implemented") + } - /** - * Expresses an error thrown by this driver in a format compatible with the Widget API. - * @param error The error to handle. - * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, - * or undefined if it cannot be expressed as one. - */ - public processError( - error: unknown, - ): IWidgetApiErrorResponseDataDetails | undefined { - return undefined; - } + /** + * Expresses an error thrown by this driver in a format compatible with the Widget API. + * @param error The error to handle. + * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, + * or undefined if it cannot be expressed as one. + */ + public processError( + error: unknown, + ): IWidgetApiErrorResponseDataDetails | undefined { + return undefined + } } diff --git a/src/index.ts b/src/index.ts index bfdff11..3194b27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,67 +15,67 @@ limitations under the License. */ // Primary structures -export * from "./WidgetApi"; -export * from "./ClientWidgetApi"; -export * from "./Symbols"; +export * from "./WidgetApi" +export * from "./ClientWidgetApi" +export * from "./Symbols" // Transports (not sure why you'd use these directly, but might as well export all the things) -export type * from "./transport/ITransport"; -export * from "./transport/PostmessageTransport"; +export type * from "./transport/ITransport" +export * from "./transport/PostmessageTransport" // Interfaces and simple models -export type * from "./interfaces/ICustomWidgetData"; -export type * from "./interfaces/IJitsiWidgetData"; -export type * from "./interfaces/IStickerpickerWidgetData"; -export type * from "./interfaces/IWidget"; -export * from "./interfaces/WidgetType"; -export * from "./interfaces/IWidgetApiErrorResponse"; -export type * from "./interfaces/IWidgetApiRequest"; -export type * from "./interfaces/IWidgetApiResponse"; -export * from "./interfaces/WidgetApiAction"; -export * from "./interfaces/WidgetApiDirection"; -export * from "./interfaces/ApiVersion"; -export * from "./interfaces/Capabilities"; -export type * from "./interfaces/CapabilitiesAction"; -export type * from "./interfaces/ContentLoadedAction"; -export type * from "./interfaces/ScreenshotAction"; -export type * from "./interfaces/StickerAction"; -export type * from "./interfaces/StickyAction"; -export type * from "./interfaces/SupportedVersionsAction"; -export type * from "./interfaces/VisibilityAction"; -export * from "./interfaces/GetOpenIDAction"; -export type * from "./interfaces/OpenIDCredentialsAction"; -export * from "./interfaces/WidgetKind"; -export * from "./interfaces/ModalButtonKind"; -export * from "./interfaces/ModalWidgetActions"; -export type * from "./interfaces/SetModalButtonEnabledAction"; -export type * from "./interfaces/WidgetConfigAction"; -export type * from "./interfaces/SendEventAction"; -export type * from "./interfaces/SendToDeviceAction"; -export type * from "./interfaces/ReadEventAction"; -export type * from "./interfaces/IRoomEvent"; -export type * from "./interfaces/IRoomAccountData"; -export type * from "./interfaces/NavigateAction"; -export type * from "./interfaces/TurnServerActions"; -export type * from "./interfaces/ReadRelationsAction"; -export type * from "./interfaces/GetMediaConfigAction"; -export * from "./interfaces/UpdateDelayedEventAction"; -export type * from "./interfaces/UpdateStateAction"; -export type * from "./interfaces/UploadFileAction"; -export type * from "./interfaces/DownloadFileAction"; -export type * from "./interfaces/ThemeChangeAction"; -export type * from "./interfaces/LanguageChangeAction"; +export type * from "./interfaces/ICustomWidgetData" +export type * from "./interfaces/IJitsiWidgetData" +export type * from "./interfaces/IStickerpickerWidgetData" +export type * from "./interfaces/IWidget" +export * from "./interfaces/WidgetType" +export * from "./interfaces/IWidgetApiErrorResponse" +export type * from "./interfaces/IWidgetApiRequest" +export type * from "./interfaces/IWidgetApiResponse" +export * from "./interfaces/WidgetApiAction" +export * from "./interfaces/WidgetApiDirection" +export * from "./interfaces/ApiVersion" +export * from "./interfaces/Capabilities" +export type * from "./interfaces/CapabilitiesAction" +export type * from "./interfaces/ContentLoadedAction" +export type * from "./interfaces/ScreenshotAction" +export type * from "./interfaces/StickerAction" +export type * from "./interfaces/StickyAction" +export type * from "./interfaces/SupportedVersionsAction" +export type * from "./interfaces/VisibilityAction" +export * from "./interfaces/GetOpenIDAction" +export type * from "./interfaces/OpenIDCredentialsAction" +export * from "./interfaces/WidgetKind" +export * from "./interfaces/ModalButtonKind" +export * from "./interfaces/ModalWidgetActions" +export type * from "./interfaces/SetModalButtonEnabledAction" +export type * from "./interfaces/WidgetConfigAction" +export type * from "./interfaces/SendEventAction" +export type * from "./interfaces/SendToDeviceAction" +export type * from "./interfaces/ReadEventAction" +export type * from "./interfaces/IRoomEvent" +export type * from "./interfaces/IRoomAccountData" +export type * from "./interfaces/NavigateAction" +export type * from "./interfaces/TurnServerActions" +export type * from "./interfaces/ReadRelationsAction" +export type * from "./interfaces/GetMediaConfigAction" +export * from "./interfaces/UpdateDelayedEventAction" +export type * from "./interfaces/UpdateStateAction" +export type * from "./interfaces/UploadFileAction" +export type * from "./interfaces/DownloadFileAction" +export type * from "./interfaces/ThemeChangeAction" +export type * from "./interfaces/LanguageChangeAction" // Complex models -export * from "./models/WidgetEventCapability"; -export * from "./models/validation/url"; -export * from "./models/validation/utils"; -export * from "./models/Widget"; -export * from "./models/WidgetParser"; +export * from "./models/WidgetEventCapability" +export * from "./models/validation/url" +export * from "./models/validation/utils" +export * from "./models/Widget" +export * from "./models/WidgetParser" // Utilities -export * from "./templating/url-template"; -export * from "./util/SimpleObservable"; +export * from "./templating/url-template" +export * from "./util/SimpleObservable" // Drivers -export * from "./driver/WidgetDriver"; +export * from "./driver/WidgetDriver" diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index fa4dd8a..388d633 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -15,40 +15,40 @@ */ export enum MatrixApiVersion { - Prerelease1 = "0.0.1", - Prerelease2 = "0.0.2", - //V010 = "0.1.0", // first release + Prerelease1 = "0.0.1", + Prerelease2 = "0.0.2", + //V010 = "0.1.0", // first release } export enum UnstableApiVersion { - MSC2762 = "org.matrix.msc2762", - MSC2871 = "org.matrix.msc2871", - MSC2873 = "org.matrix.msc2873", - MSC2931 = "org.matrix.msc2931", - MSC2974 = "org.matrix.msc2974", - MSC2876 = "org.matrix.msc2876", - MSC3819 = "org.matrix.msc3819", - MSC3846 = "town.robin.msc3846", - MSC3869 = "org.matrix.msc3869", - MSC3973 = "org.matrix.msc3973", - MSC4039 = "org.matrix.msc4039", + MSC2762 = "org.matrix.msc2762", + MSC2871 = "org.matrix.msc2871", + MSC2873 = "org.matrix.msc2873", + MSC2931 = "org.matrix.msc2931", + MSC2974 = "org.matrix.msc2974", + MSC2876 = "org.matrix.msc2876", + MSC3819 = "org.matrix.msc3819", + MSC3846 = "town.robin.msc3846", + MSC3869 = "org.matrix.msc3869", + MSC3973 = "org.matrix.msc3973", + MSC4039 = "org.matrix.msc4039", } -export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string; +export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string export const CurrentApiVersions: ApiVersion[] = [ - MatrixApiVersion.Prerelease1, - MatrixApiVersion.Prerelease2, - //MatrixApiVersion.V010, - UnstableApiVersion.MSC2762, - UnstableApiVersion.MSC2871, - UnstableApiVersion.MSC2873, - UnstableApiVersion.MSC2931, - UnstableApiVersion.MSC2974, - UnstableApiVersion.MSC2876, - UnstableApiVersion.MSC3819, - UnstableApiVersion.MSC3846, - UnstableApiVersion.MSC3869, - UnstableApiVersion.MSC3973, - UnstableApiVersion.MSC4039, -]; + MatrixApiVersion.Prerelease1, + MatrixApiVersion.Prerelease2, + //MatrixApiVersion.V010, + UnstableApiVersion.MSC2762, + UnstableApiVersion.MSC2871, + UnstableApiVersion.MSC2873, + UnstableApiVersion.MSC2931, + UnstableApiVersion.MSC2974, + UnstableApiVersion.MSC2876, + UnstableApiVersion.MSC3819, + UnstableApiVersion.MSC3846, + UnstableApiVersion.MSC3869, + UnstableApiVersion.MSC3973, + UnstableApiVersion.MSC4039, +] diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index 1c0c1a6..df414d4 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -14,52 +14,52 @@ * limitations under the License. */ -import { Symbols } from "../Symbols"; +import { Symbols } from "../Symbols" export enum MatrixCapabilities { - Screenshots = "m.capability.screenshot", - StickerSending = "m.sticker", - AlwaysOnScreen = "m.always_on_screen", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - * Ask Element to not give the option to move the widget into a separate tab. - */ - RequiresClient = "io.element.requires_client", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2931Navigate = "org.matrix.msc2931.navigate", - MSC3846TurnServers = "town.robin.msc3846.turn_servers", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039UploadFile = "org.matrix.msc4039.upload_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039DownloadFile = "org.matrix.msc4039.download_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", + Screenshots = "m.capability.screenshot", + StickerSending = "m.sticker", + AlwaysOnScreen = "m.always_on_screen", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + * Ask Element to not give the option to move the widget into a separate tab. + */ + RequiresClient = "io.element.requires_client", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2931Navigate = "org.matrix.msc2931.navigate", + MSC3846TurnServers = "town.robin.msc3846.turn_servers", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFile = "org.matrix.msc4039.upload_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039DownloadFile = "org.matrix.msc4039.download_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157SendDelayedEvent = "org.matrix.msc4157.send.delayed_event", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } -export type Capability = MatrixCapabilities | string; +export type Capability = MatrixCapabilities | string export const StickerpickerCapabilities: Capability[] = [ - MatrixCapabilities.StickerSending, -]; + MatrixCapabilities.StickerSending, +] export const VideoConferenceCapabilities: Capability[] = [ - MatrixCapabilities.AlwaysOnScreen, -]; + MatrixCapabilities.AlwaysOnScreen, +] /** * Determines if a capability is a capability for a timeline. @@ -67,8 +67,8 @@ export const VideoConferenceCapabilities: Capability[] = [ * @returns {boolean} True if a timeline capability, false otherwise. */ export function isTimelineCapability(capability: Capability): boolean { - // TODO: Change when MSC2762 becomes stable. - return capability?.startsWith("org.matrix.msc2762.timeline:"); + // TODO: Change when MSC2762 becomes stable. + return capability?.startsWith("org.matrix.msc2762.timeline:") } /** @@ -78,10 +78,10 @@ export function isTimelineCapability(capability: Capability): boolean { * @returns {boolean} True if a matching capability, false otherwise. */ export function isTimelineCapabilityFor( - capability: Capability, - roomId: string | Symbols.AnyRoom, + capability: Capability, + roomId: string | Symbols.AnyRoom, ): boolean { - return capability === `org.matrix.msc2762.timeline:${roomId}`; + return capability === `org.matrix.msc2762.timeline:${roomId}` } /** @@ -90,7 +90,7 @@ export function isTimelineCapabilityFor( * @returns {string} The room ID. */ export function getTimelineRoomIDFromCapability( - capability: Capability, + capability: Capability, ): string { - return capability.substring(capability.indexOf(":") + 1); + return capability.substring(capability.indexOf(":") + 1) } diff --git a/src/interfaces/CapabilitiesAction.ts b/src/interfaces/CapabilitiesAction.ts index cff440b..7022d17 100644 --- a/src/interfaces/CapabilitiesAction.ts +++ b/src/interfaces/CapabilitiesAction.ts @@ -15,63 +15,63 @@ */ import { - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest"; + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; -import { Capability } from "./Capabilities"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" +import { Capability } from "./Capabilities" import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponseData, -} from "./IWidgetApiResponse"; + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponseData, +} from "./IWidgetApiResponse" export interface ICapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.Capabilities; - data: IWidgetApiRequestEmptyData; + action: WidgetApiToWidgetAction.Capabilities + data: IWidgetApiRequestEmptyData } export interface ICapabilitiesActionResponseData - extends IWidgetApiResponseData { - capabilities: Capability[]; + extends IWidgetApiResponseData { + capabilities: Capability[] } export interface ICapabilitiesActionResponse - extends ICapabilitiesActionRequest { - response: ICapabilitiesActionResponseData; + extends ICapabilitiesActionRequest { + response: ICapabilitiesActionResponseData } export interface INotifyCapabilitiesActionRequestData - extends IWidgetApiRequestData { - requested: Capability[]; - approved: Capability[]; + extends IWidgetApiRequestData { + requested: Capability[] + approved: Capability[] } export interface INotifyCapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.NotifyCapabilities; - data: INotifyCapabilitiesActionRequestData; + action: WidgetApiToWidgetAction.NotifyCapabilities + data: INotifyCapabilitiesActionRequestData } export interface INotifyCapabilitiesActionResponse - extends INotifyCapabilitiesActionRequest { - response: IWidgetApiAcknowledgeResponseData; + extends INotifyCapabilitiesActionRequest { + response: IWidgetApiAcknowledgeResponseData } export interface IRenegotiateCapabilitiesActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities; - data: IRenegotiateCapabilitiesRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities + data: IRenegotiateCapabilitiesRequestData } export interface IRenegotiateCapabilitiesRequestData - extends IWidgetApiResponseData { - capabilities: Capability[]; + extends IWidgetApiResponseData { + capabilities: Capability[] } export interface IRenegotiateCapabilitiesActionResponse - extends IRenegotiateCapabilitiesActionRequest { - // nothing + extends IRenegotiateCapabilitiesActionRequest { + // nothing } diff --git a/src/interfaces/ContentLoadedAction.ts b/src/interfaces/ContentLoadedAction.ts index d5ae581..709aba2 100644 --- a/src/interfaces/ContentLoadedAction.ts +++ b/src/interfaces/ContentLoadedAction.ts @@ -15,18 +15,18 @@ */ import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface IContentLoadedActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.ContentLoaded; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.ContentLoaded + data: IWidgetApiRequestEmptyData } export interface IContentLoadedActionResponse - extends IContentLoadedActionRequest { - response: IWidgetApiAcknowledgeResponseData; + extends IContentLoadedActionRequest { + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/DownloadFileAction.ts b/src/interfaces/DownloadFileAction.ts index a678068..3b0f540 100644 --- a/src/interfaces/DownloadFileAction.ts +++ b/src/interfaces/DownloadFileAction.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" export interface IDownloadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - content_uri: string; // eslint-disable-line camelcase + extends IWidgetApiRequestData { + content_uri: string // eslint-disable-line camelcase } export interface IDownloadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; - data: IDownloadFileActionFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction + data: IDownloadFileActionFromWidgetRequestData } export interface IDownloadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - file: XMLHttpRequestBodyInit; + extends IWidgetApiResponseData { + file: XMLHttpRequestBodyInit } export interface IDownloadFileActionFromWidgetActionResponse - extends IDownloadFileActionFromWidgetActionRequest { - response: IDownloadFileActionFromWidgetResponseData; + extends IDownloadFileActionFromWidgetActionRequest { + response: IDownloadFileActionFromWidgetResponseData } diff --git a/src/interfaces/GetMediaConfigAction.ts b/src/interfaces/GetMediaConfigAction.ts index f67c2c8..2ce036e 100644 --- a/src/interfaces/GetMediaConfigAction.ts +++ b/src/interfaces/GetMediaConfigAction.ts @@ -14,25 +14,25 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" export interface IGetMediaConfigActionFromWidgetRequestData - extends IWidgetApiRequestData {} + extends IWidgetApiRequestData {} export interface IGetMediaConfigActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; - data: IGetMediaConfigActionFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction + data: IGetMediaConfigActionFromWidgetRequestData } export interface IGetMediaConfigActionFromWidgetResponseData - extends IWidgetApiResponseData { - "m.upload.size"?: number; + extends IWidgetApiResponseData { + "m.upload.size"?: number } export interface IGetMediaConfigActionFromWidgetActionResponse - extends IGetMediaConfigActionFromWidgetActionRequest { - response: IGetMediaConfigActionFromWidgetResponseData; + extends IGetMediaConfigActionFromWidgetActionRequest { + response: IGetMediaConfigActionFromWidgetResponseData } diff --git a/src/interfaces/GetOpenIDAction.ts b/src/interfaces/GetOpenIDAction.ts index 024829e..e673846 100644 --- a/src/interfaces/GetOpenIDAction.ts +++ b/src/interfaces/GetOpenIDAction.ts @@ -14,38 +14,38 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" export enum OpenIDRequestState { - Allowed = "allowed", - Blocked = "blocked", - PendingUserConfirmation = "request", + Allowed = "allowed", + Blocked = "blocked", + PendingUserConfirmation = "request", } export interface IOpenIDCredentials { - access_token?: string; // eslint-disable-line camelcase - expires_in?: number; // eslint-disable-line camelcase - matrix_server_name?: string; // eslint-disable-line camelcase - token_type?: "Bearer" | string; // eslint-disable-line camelcase + access_token?: string // eslint-disable-line camelcase + expires_in?: number // eslint-disable-line camelcase + matrix_server_name?: string // eslint-disable-line camelcase + token_type?: "Bearer" | string // eslint-disable-line camelcase } export interface IGetOpenIDActionRequestData extends IWidgetApiRequestData { - // nothing + // nothing } export interface IGetOpenIDActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.GetOpenIDCredentials; - data: IGetOpenIDActionRequestData; + action: WidgetApiFromWidgetAction.GetOpenIDCredentials + data: IGetOpenIDActionRequestData } export interface IGetOpenIDActionResponseData - extends IWidgetApiResponseData, - IOpenIDCredentials { - state: OpenIDRequestState; + extends IWidgetApiResponseData, + IOpenIDCredentials { + state: OpenIDRequestState } export interface IGetOpenIDActionResponse extends IGetOpenIDActionRequest { - response: IGetOpenIDActionResponseData; + response: IGetOpenIDActionResponseData } diff --git a/src/interfaces/ICustomWidgetData.ts b/src/interfaces/ICustomWidgetData.ts index b9360e5..a3d93e3 100644 --- a/src/interfaces/ICustomWidgetData.ts +++ b/src/interfaces/ICustomWidgetData.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget"; +import { IWidgetData } from "./IWidget" /** * Widget data for m.custom specifically. */ export interface ICustomWidgetData extends IWidgetData { - /** - * The URL for the widget if the templated URL is not exactly what will be loaded. - */ - url?: string; + /** + * The URL for the widget if the templated URL is not exactly what will be loaded. + */ + url?: string } diff --git a/src/interfaces/IJitsiWidgetData.ts b/src/interfaces/IJitsiWidgetData.ts index 414ba0d..3764594 100644 --- a/src/interfaces/IJitsiWidgetData.ts +++ b/src/interfaces/IJitsiWidgetData.ts @@ -14,25 +14,25 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget"; +import { IWidgetData } from "./IWidget" /** * Widget data for m.jitsi widgets. */ export interface IJitsiWidgetData extends IWidgetData { - /** - * The domain where the Jitsi Meet conference is being held. - */ - domain: string; + /** + * The domain where the Jitsi Meet conference is being held. + */ + domain: string - /** - * The conference ID (also known as the room name) where the conference is being held. - */ - conferenceId: string; + /** + * The conference ID (also known as the room name) where the conference is being held. + */ + conferenceId: string - /** - * Optional. True to indicate that the conference should be without video, false - * otherwise (default). - */ - isAudioOnly?: boolean; + /** + * Optional. True to indicate that the conference should be without video, false + * otherwise (default). + */ + isAudioOnly?: boolean } diff --git a/src/interfaces/IRoomAccountData.ts b/src/interfaces/IRoomAccountData.ts index f29a8ec..18682df 100644 --- a/src/interfaces/IRoomAccountData.ts +++ b/src/interfaces/IRoomAccountData.ts @@ -15,7 +15,7 @@ */ export interface IRoomAccountData { - type: string; - room_id: string; // eslint-disable-line camelcase - content: unknown; + type: string + room_id: string // eslint-disable-line camelcase + content: unknown } diff --git a/src/interfaces/IRoomEvent.ts b/src/interfaces/IRoomEvent.ts index 6df0336..1d90f53 100644 --- a/src/interfaces/IRoomEvent.ts +++ b/src/interfaces/IRoomEvent.ts @@ -15,12 +15,12 @@ */ export interface IRoomEvent { - type: string; - sender: string; - event_id: string; // eslint-disable-line camelcase - room_id: string; // eslint-disable-line camelcase - state_key?: string; // eslint-disable-line camelcase - origin_server_ts: number; // eslint-disable-line camelcase - content: unknown; - unsigned: unknown; + type: string + sender: string + event_id: string // eslint-disable-line camelcase + room_id: string // eslint-disable-line camelcase + state_key?: string // eslint-disable-line camelcase + origin_server_ts: number // eslint-disable-line camelcase + content: unknown + unsigned: unknown } diff --git a/src/interfaces/IStickerpickerWidgetData.ts b/src/interfaces/IStickerpickerWidgetData.ts index 816ca14..729b22b 100644 --- a/src/interfaces/IStickerpickerWidgetData.ts +++ b/src/interfaces/IStickerpickerWidgetData.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget"; +import { IWidgetData } from "./IWidget" export interface IStickerpickerWidgetData extends IWidgetData { - // no additional properties (for now) + // no additional properties (for now) } diff --git a/src/interfaces/IWidget.ts b/src/interfaces/IWidget.ts index 72893f4..d6e7f98 100644 --- a/src/interfaces/IWidget.ts +++ b/src/interfaces/IWidget.ts @@ -14,21 +14,21 @@ * limitations under the License. */ -import { WidgetType } from "./WidgetType"; +import { WidgetType } from "./WidgetType" /** * Widget data. */ export interface IWidgetData { - /** - * Optional title for the widget. - */ - title?: string; + /** + * Optional title for the widget. + */ + title?: string - /** - * Custom keys for inclusion in the template URL. - */ - [key: string]: unknown; + /** + * Custom keys for inclusion in the template URL. + */ + [key: string]: unknown } /** @@ -36,40 +36,40 @@ export interface IWidgetData { * https://matrix.org/docs/spec/widgets/latest#widgetcommonproperties-schema */ export interface IWidget { - /** - * The ID of the widget. - */ - id: string; + /** + * The ID of the widget. + */ + id: string - /** - * The user ID who originally created the widget. - */ - creatorUserId: string; + /** + * The user ID who originally created the widget. + */ + creatorUserId: string - /** - * Optional name for the widget. - */ - name?: string; + /** + * Optional name for the widget. + */ + name?: string - /** - * The type of widget. - */ - type: WidgetType; + /** + * The type of widget. + */ + type: WidgetType - /** - * The URL for the widget, with template variables. - */ - url: string; + /** + * The URL for the widget, with template variables. + */ + url: string - /** - * Optional flag to indicate whether or not the client should initiate communication - * right after the iframe loads (default, true) or when the widget indicates it is - * ready (false). - */ - waitForIframeLoad?: boolean; + /** + * Optional flag to indicate whether or not the client should initiate communication + * right after the iframe loads (default, true) or when the widget indicates it is + * ready (false). + */ + waitForIframeLoad?: boolean - /** - * Data for the widget. - */ - data?: IWidgetData; + /** + * Data for the widget. + */ + data?: IWidgetData } diff --git a/src/interfaces/IWidgetApiErrorResponse.ts b/src/interfaces/IWidgetApiErrorResponse.ts index 935ba6d..ee92482 100644 --- a/src/interfaces/IWidgetApiErrorResponse.ts +++ b/src/interfaces/IWidgetApiErrorResponse.ts @@ -15,52 +15,52 @@ */ import { - IWidgetApiResponse, - IWidgetApiResponseData, -} from "./IWidgetApiResponse"; + IWidgetApiResponse, + IWidgetApiResponseData, +} from "./IWidgetApiResponse" /** * The format of errors returned by Matrix API requests * made by a WidgetDriver. */ export interface IMatrixApiError { - /** The HTTP status code of the associated request. */ - http_status: number; // eslint-disable-line camelcase - /** Any HTTP response headers that are relevant to the error. */ - http_headers: { [name: string]: string }; // eslint-disable-line camelcase - /** The URL of the failed request. */ - url: string; - /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ - response: { - errcode: string; - error: string; - } & IWidgetApiResponseData; // extensible + /** The HTTP status code of the associated request. */ + http_status: number // eslint-disable-line camelcase + /** Any HTTP response headers that are relevant to the error. */ + http_headers: { [name: string]: string } // eslint-disable-line camelcase + /** The URL of the failed request. */ + url: string + /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ + response: { + errcode: string + error: string + } & IWidgetApiResponseData // extensible } export interface IWidgetApiErrorResponseDataDetails { - /** Set if the error came from a Matrix API request made by a widget driver */ - matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase + /** Set if the error came from a Matrix API request made by a widget driver */ + matrix_api_error?: IMatrixApiError // eslint-disable-line camelcase } export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData { - error: { - /** A user-friendly string describing the error */ - message: string; - } & IWidgetApiErrorResponseDataDetails; + error: { + /** A user-friendly string describing the error */ + message: string + } & IWidgetApiErrorResponseDataDetails } export interface IWidgetApiErrorResponse extends IWidgetApiResponse { - response: IWidgetApiErrorResponseData; + response: IWidgetApiErrorResponseData } export function isErrorResponse( - responseData: IWidgetApiResponseData, + responseData: IWidgetApiResponseData, ): responseData is IWidgetApiErrorResponseData { - const error = responseData.error; - return ( - typeof error === "object" && - error !== null && - "message" in error && - typeof error.message === "string" - ); + const error = responseData.error + return ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ) } diff --git a/src/interfaces/IWidgetApiRequest.ts b/src/interfaces/IWidgetApiRequest.ts index 4574035..8af787e 100644 --- a/src/interfaces/IWidgetApiRequest.ts +++ b/src/interfaces/IWidgetApiRequest.ts @@ -14,25 +14,25 @@ * limitations under the License. */ -import { WidgetApiDirection } from "./WidgetApiDirection"; -import { WidgetApiAction } from "./WidgetApiAction"; +import { WidgetApiDirection } from "./WidgetApiDirection" +import { WidgetApiAction } from "./WidgetApiAction" export interface IWidgetApiRequestData { - [key: string]: unknown; + [key: string]: unknown } export interface IWidgetApiRequestEmptyData extends IWidgetApiRequestData { - // nothing + // nothing } export interface IWidgetApiRequest { - api: WidgetApiDirection; - requestId: string; - action: WidgetApiAction; - widgetId: string; - data: IWidgetApiRequestData; - // XXX: This is for Scalar support - // TODO: Fix scalar - // eslint-disable-next-line @typescript-eslint/no-explicit-any - visible?: any; + api: WidgetApiDirection + requestId: string + action: WidgetApiAction + widgetId: string + data: IWidgetApiRequestData + // XXX: This is for Scalar support + // TODO: Fix scalar + // eslint-disable-next-line @typescript-eslint/no-explicit-any + visible?: any } diff --git a/src/interfaces/IWidgetApiResponse.ts b/src/interfaces/IWidgetApiResponse.ts index 07dbc64..e7b63ff 100644 --- a/src/interfaces/IWidgetApiResponse.ts +++ b/src/interfaces/IWidgetApiResponse.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { IWidgetApiRequest } from "./IWidgetApiRequest"; +import { IWidgetApiRequest } from "./IWidgetApiRequest" export interface IWidgetApiResponseData { - [key: string]: unknown; + [key: string]: unknown } export interface IWidgetApiAcknowledgeResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface IWidgetApiResponse extends IWidgetApiRequest { - response: IWidgetApiResponseData; + response: IWidgetApiResponseData } diff --git a/src/interfaces/LanguageChangeAction.ts b/src/interfaces/LanguageChangeAction.ts index feec8fb..593d6f4 100644 --- a/src/interfaces/LanguageChangeAction.ts +++ b/src/interfaces/LanguageChangeAction.ts @@ -14,24 +14,24 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface ILanguageChangeActionRequestData - extends IWidgetApiRequestData { - /** - * The BCP 47 identifier for the client's current language. - */ - lang: string; + extends IWidgetApiRequestData { + /** + * The BCP 47 identifier for the client's current language. + */ + lang: string } export interface ILanguageChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.LanguageChange; - data: ILanguageChangeActionRequestData; + action: WidgetApiToWidgetAction.LanguageChange + data: ILanguageChangeActionRequestData } export interface ILanguageChangeActionResponse - extends ILanguageChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData; + extends ILanguageChangeActionRequest { + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/ModalButtonKind.ts b/src/interfaces/ModalButtonKind.ts index a6a304c..e82c939 100644 --- a/src/interfaces/ModalButtonKind.ts +++ b/src/interfaces/ModalButtonKind.ts @@ -15,9 +15,9 @@ */ export enum ModalButtonKind { - Primary = "m.primary", - Secondary = "m.secondary", - Warning = "m.warning", - Danger = "m.danger", - Link = "m.link", + Primary = "m.primary", + Secondary = "m.secondary", + Warning = "m.warning", + Danger = "m.danger", + Link = "m.link", } diff --git a/src/interfaces/ModalWidgetActions.ts b/src/interfaces/ModalWidgetActions.ts index 0073d0c..319d49e 100644 --- a/src/interfaces/ModalWidgetActions.ts +++ b/src/interfaces/ModalWidgetActions.ts @@ -14,87 +14,87 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse"; -import { IWidget } from "./IWidget"; -import { ModalButtonKind } from "./ModalButtonKind"; + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse" +import { IWidget } from "./IWidget" +import { ModalButtonKind } from "./ModalButtonKind" export enum BuiltInModalButtonID { - Close = "m.close", + Close = "m.close", } -export type ModalButtonID = BuiltInModalButtonID | string; +export type ModalButtonID = BuiltInModalButtonID | string export interface IModalWidgetCreateData extends IWidgetApiRequestData { - [key: string]: unknown; + [key: string]: unknown } export interface IModalWidgetReturnData { - [key: string]: unknown; + [key: string]: unknown } // Types for a normal modal requesting the opening a modal widget export interface IModalWidgetOpenRequestDataButton { - id: ModalButtonID; - label: string; - kind: ModalButtonKind | string; - disabled?: boolean; + id: ModalButtonID + label: string + kind: ModalButtonKind | string + disabled?: boolean } export interface IModalWidgetOpenRequestData - extends IModalWidgetCreateData, - Omit { - buttons?: IModalWidgetOpenRequestDataButton[]; + extends IModalWidgetCreateData, + Omit { + buttons?: IModalWidgetOpenRequestDataButton[] } export interface IModalWidgetOpenRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.OpenModalWidget; - data: IModalWidgetOpenRequestData; + action: WidgetApiFromWidgetAction.OpenModalWidget + data: IModalWidgetOpenRequestData } export interface IModalWidgetOpenResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } // Types for a modal widget receiving notifications that its buttons have been pressed export interface IModalWidgetButtonClickedRequestData - extends IWidgetApiRequestData { - id: IModalWidgetOpenRequestDataButton["id"]; + extends IWidgetApiRequestData { + id: IModalWidgetOpenRequestDataButton["id"] } export interface IModalWidgetButtonClickedRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ButtonClicked; - data: IModalWidgetButtonClickedRequestData; + action: WidgetApiToWidgetAction.ButtonClicked + data: IModalWidgetButtonClickedRequestData } export interface IModalWidgetButtonClickedResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } // Types for a modal widget requesting close export interface IModalWidgetCloseRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.CloseModalWidget; - data: IModalWidgetReturnData; + action: WidgetApiFromWidgetAction.CloseModalWidget + data: IModalWidgetReturnData } export interface IModalWidgetCloseResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } // Types for a normal widget being notified that the modal widget it opened has been closed export interface IModalWidgetCloseNotificationRequest - extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.CloseModalWidget; - data: IModalWidgetReturnData; + extends IWidgetApiRequest { + action: WidgetApiToWidgetAction.CloseModalWidget + data: IModalWidgetReturnData } export interface IModalWidgetCloseNotificationResponse - extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + extends IWidgetApiResponse { + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/NavigateAction.ts b/src/interfaces/NavigateAction.ts index dd6663e..fc23a64 100644 --- a/src/interfaces/NavigateAction.ts +++ b/src/interfaces/NavigateAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface INavigateActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2931Navigate; - data: INavigateActionRequestData; + action: WidgetApiFromWidgetAction.MSC2931Navigate + data: INavigateActionRequestData } export interface INavigateActionRequestData extends IWidgetApiRequestData { - uri: string; + uri: string } export interface INavigateActionResponse extends INavigateActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/OpenIDCredentialsAction.ts b/src/interfaces/OpenIDCredentialsAction.ts index d079208..d0adb41 100644 --- a/src/interfaces/OpenIDCredentialsAction.ts +++ b/src/interfaces/OpenIDCredentialsAction.ts @@ -14,29 +14,29 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IOpenIDCredentials, OpenIDRequestState } from "./GetOpenIDAction"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IOpenIDCredentials, OpenIDRequestState } from "./GetOpenIDAction" export interface IOpenIDCredentialsActionRequestData - extends IWidgetApiRequestData, - IOpenIDCredentials { - state: OpenIDRequestState; - original_request_id: string; // eslint-disable-line camelcase + extends IWidgetApiRequestData, + IOpenIDCredentials { + state: OpenIDRequestState + original_request_id: string // eslint-disable-line camelcase } export interface IOpenIDCredentialsActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.OpenIDCredentials; - data: IOpenIDCredentialsActionRequestData; + action: WidgetApiToWidgetAction.OpenIDCredentials + data: IOpenIDCredentialsActionRequestData } export interface IOpenIDCredentialsActionResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface IOpenIDCredentialsIDActionResponse - extends IOpenIDCredentialsActionRequest { - response: IOpenIDCredentialsActionResponseData; + extends IOpenIDCredentialsActionRequest { + response: IOpenIDCredentialsActionResponseData } diff --git a/src/interfaces/ReadEventAction.ts b/src/interfaces/ReadEventAction.ts index d1fdd87..f969235 100644 --- a/src/interfaces/ReadEventAction.ts +++ b/src/interfaces/ReadEventAction.ts @@ -14,32 +14,32 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IRoomEvent } from "./IRoomEvent"; -import { Symbols } from "../Symbols"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IRoomEvent } from "./IRoomEvent" +import { Symbols } from "../Symbols" export interface IReadEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string | boolean; // eslint-disable-line camelcase - msgtype?: string; - type: string; - limit?: number; - room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase - since?: string; + state_key?: string | boolean // eslint-disable-line camelcase + msgtype?: string + type: string + limit?: number + room_ids?: Symbols.AnyRoom | string[] // eslint-disable-line camelcase + since?: string } export interface IReadEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2876ReadEvents; - data: IReadEventFromWidgetRequestData; + action: WidgetApiFromWidgetAction.MSC2876ReadEvents + data: IReadEventFromWidgetRequestData } export interface IReadEventFromWidgetResponseData - extends IWidgetApiResponseData { - events: IRoomEvent[]; + extends IWidgetApiResponseData { + events: IRoomEvent[] } export interface IReadEventFromWidgetActionResponse - extends IReadEventFromWidgetActionRequest { - response: IReadEventFromWidgetResponseData; + extends IReadEventFromWidgetActionRequest { + response: IReadEventFromWidgetResponseData } diff --git a/src/interfaces/ReadRelationsAction.ts b/src/interfaces/ReadRelationsAction.ts index 7081756..3fa3772 100644 --- a/src/interfaces/ReadRelationsAction.ts +++ b/src/interfaces/ReadRelationsAction.ts @@ -14,39 +14,39 @@ * limitations under the License. */ -import { IRoomEvent } from "./IRoomEvent"; -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IRoomEvent } from "./IRoomEvent" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" export interface IReadRelationsFromWidgetRequestData - extends IWidgetApiRequestData { - event_id: string; // eslint-disable-line camelcase - rel_type?: string; // eslint-disable-line camelcase - event_type?: string; // eslint-disable-line camelcase - room_id?: string; // eslint-disable-line camelcase + extends IWidgetApiRequestData { + event_id: string // eslint-disable-line camelcase + rel_type?: string // eslint-disable-line camelcase + event_type?: string // eslint-disable-line camelcase + room_id?: string // eslint-disable-line camelcase - limit?: number; - from?: string; - to?: string; - direction?: "f" | "b"; + limit?: number + from?: string + to?: string + direction?: "f" | "b" } export interface IReadRelationsFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3869ReadRelations; - data: IReadRelationsFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3869ReadRelations + data: IReadRelationsFromWidgetRequestData } export interface IReadRelationsFromWidgetResponseData - extends IWidgetApiResponseData { - chunk: IRoomEvent[]; + extends IWidgetApiResponseData { + chunk: IRoomEvent[] - next_batch?: string; // eslint-disable-line camelcase - prev_batch?: string; // eslint-disable-line camelcase + next_batch?: string // eslint-disable-line camelcase + prev_batch?: string // eslint-disable-line camelcase } export interface IReadRelationsFromWidgetActionResponse - extends IReadRelationsFromWidgetActionRequest { - response: IReadRelationsFromWidgetResponseData; + extends IReadRelationsFromWidgetActionRequest { + response: IReadRelationsFromWidgetResponseData } diff --git a/src/interfaces/ReadRoomAccountDataAction.ts b/src/interfaces/ReadRoomAccountDataAction.ts index 43c5204..743eba1 100644 --- a/src/interfaces/ReadRoomAccountDataAction.ts +++ b/src/interfaces/ReadRoomAccountDataAction.ts @@ -14,30 +14,30 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IRoomAccountData } from "./IRoomAccountData"; -import { Symbols } from "../Symbols"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IRoomAccountData } from "./IRoomAccountData" +import { Symbols } from "../Symbols" export interface IReadRoomAccountDataFromWidgetRequestData - extends IWidgetApiRequestData { - type: string; - room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase + extends IWidgetApiRequestData { + type: string + room_ids?: Symbols.AnyRoom | string[] // eslint-disable-line camelcase } export interface IReadRoomAccountDataFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData; - data: IReadRoomAccountDataFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData + data: IReadRoomAccountDataFromWidgetRequestData } export interface IReadRoomAccountDataFromWidgetResponseData - extends IWidgetApiResponseData { - events: IRoomAccountData[]; + extends IWidgetApiResponseData { + events: IRoomAccountData[] } export interface IReadRoomAccountDataFromWidgetActionResponse - extends IReadRoomAccountDataFromWidgetActionRequest { - response: IReadRoomAccountDataFromWidgetResponseData; + extends IReadRoomAccountDataFromWidgetActionRequest { + response: IReadRoomAccountDataFromWidgetResponseData } diff --git a/src/interfaces/ScreenshotAction.ts b/src/interfaces/ScreenshotAction.ts index bfa3008..96a0644 100644 --- a/src/interfaces/ScreenshotAction.ts +++ b/src/interfaces/ScreenshotAction.ts @@ -15,21 +15,21 @@ */ import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" export interface IScreenshotActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.TakeScreenshot; - data: IWidgetApiRequestEmptyData; + action: WidgetApiToWidgetAction.TakeScreenshot + data: IWidgetApiRequestEmptyData } export interface IScreenshotActionResponseData extends IWidgetApiResponseData { - screenshot: Blob; + screenshot: Blob } export interface IScreenshotActionResponse extends IScreenshotActionRequest { - response: IScreenshotActionResponseData; + response: IScreenshotActionResponseData } diff --git a/src/interfaces/SendEventAction.ts b/src/interfaces/SendEventAction.ts index ba89b58..a4daa9d 100644 --- a/src/interfaces/SendEventAction.ts +++ b/src/interfaces/SendEventAction.ts @@ -14,58 +14,58 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IRoomEvent } from "./IRoomEvent"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IRoomEvent } from "./IRoomEvent" export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string; // eslint-disable-line camelcase - type: string; - content: unknown; - room_id?: string; // eslint-disable-line camelcase + state_key?: string // eslint-disable-line camelcase + type: string + content: unknown + room_id?: string // eslint-disable-line camelcase - // MSC4157 - delay?: number; // eslint-disable-line camelcase - parent_delay_id?: string; // eslint-disable-line camelcase + // MSC4157 + delay?: number // eslint-disable-line camelcase + parent_delay_id?: string // eslint-disable-line camelcase } export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendEvent; - data: ISendEventFromWidgetRequestData; + action: WidgetApiFromWidgetAction.SendEvent + data: ISendEventFromWidgetRequestData } export interface ISendEventFromWidgetResponseData - extends IWidgetApiResponseData { - room_id: string; // eslint-disable-line camelcase - event_id?: string; // eslint-disable-line camelcase + extends IWidgetApiResponseData { + room_id: string // eslint-disable-line camelcase + event_id?: string // eslint-disable-line camelcase - // MSC4157 - delay_id?: string; // eslint-disable-line camelcase + // MSC4157 + delay_id?: string // eslint-disable-line camelcase } export interface ISendEventFromWidgetActionResponse - extends ISendEventFromWidgetActionRequest { - response: ISendEventFromWidgetResponseData; + extends ISendEventFromWidgetActionRequest { + response: ISendEventFromWidgetResponseData } export interface ISendEventToWidgetRequestData - extends IWidgetApiRequestData, - IRoomEvent {} + extends IWidgetApiRequestData, + IRoomEvent {} export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendEvent; - data: ISendEventToWidgetRequestData; + action: WidgetApiToWidgetAction.SendEvent + data: ISendEventToWidgetRequestData } export interface ISendEventToWidgetResponseData extends IWidgetApiResponseData { - // nothing + // nothing } export interface ISendEventToWidgetActionResponse - extends ISendEventToWidgetActionRequest { - response: ISendEventToWidgetResponseData; + extends ISendEventToWidgetActionRequest { + response: ISendEventToWidgetResponseData } diff --git a/src/interfaces/SendToDeviceAction.ts b/src/interfaces/SendToDeviceAction.ts index 920119f..2a01528 100644 --- a/src/interfaces/SendToDeviceAction.ts +++ b/src/interfaces/SendToDeviceAction.ts @@ -14,54 +14,54 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IRoomEvent } from "./IRoomEvent"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IRoomEvent } from "./IRoomEvent" export interface ISendToDeviceFromWidgetRequestData - extends IWidgetApiRequestData { - type: string; - encrypted: boolean; - messages: { [userId: string]: { [deviceId: string]: object } }; + extends IWidgetApiRequestData { + type: string + encrypted: boolean + messages: { [userId: string]: { [deviceId: string]: object } } } export interface ISendToDeviceFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendToDevice; - data: ISendToDeviceFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.SendToDevice + data: ISendToDeviceFromWidgetRequestData } export interface ISendToDeviceFromWidgetResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface ISendToDeviceFromWidgetActionResponse - extends ISendToDeviceFromWidgetActionRequest { - response: ISendToDeviceFromWidgetResponseData; + extends ISendToDeviceFromWidgetActionRequest { + response: ISendToDeviceFromWidgetResponseData } export interface ISendToDeviceToWidgetRequestData - extends IWidgetApiRequestData, - IRoomEvent { - encrypted: boolean; + extends IWidgetApiRequestData, + IRoomEvent { + encrypted: boolean } export interface ISendToDeviceToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendToDevice; - data: ISendToDeviceToWidgetRequestData; + action: WidgetApiToWidgetAction.SendToDevice + data: ISendToDeviceToWidgetRequestData } export interface ISendToDeviceToWidgetResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface ISendToDeviceToWidgetActionResponse - extends ISendToDeviceToWidgetActionRequest { - response: ISendToDeviceToWidgetResponseData; + extends ISendToDeviceToWidgetActionRequest { + response: ISendToDeviceToWidgetResponseData } diff --git a/src/interfaces/SetModalButtonEnabledAction.ts b/src/interfaces/SetModalButtonEnabledAction.ts index ada05c6..c0eff40 100644 --- a/src/interfaces/SetModalButtonEnabledAction.ts +++ b/src/interfaces/SetModalButtonEnabledAction.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; -import { ModalButtonID } from "./ModalWidgetActions"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { ModalButtonID } from "./ModalWidgetActions" export interface ISetModalButtonEnabledActionRequestData - extends IWidgetApiRequestData { - enabled: boolean; - button: ModalButtonID; + extends IWidgetApiRequestData { + enabled: boolean + button: ModalButtonID } export interface ISetModalButtonEnabledActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SetModalButtonEnabled; - data: ISetModalButtonEnabledActionRequestData; + action: WidgetApiFromWidgetAction.SetModalButtonEnabled + data: ISetModalButtonEnabledActionRequestData } export interface ISetModalButtonEnabledActionResponse - extends ISetModalButtonEnabledActionRequest { - response: IWidgetApiAcknowledgeResponseData; + extends ISetModalButtonEnabledActionRequest { + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/StickerAction.ts b/src/interfaces/StickerAction.ts index cd401c2..0db5bff 100644 --- a/src/interfaces/StickerAction.ts +++ b/src/interfaces/StickerAction.ts @@ -14,36 +14,36 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface IStickerActionRequestData extends IWidgetApiRequestData { - name: string; - description?: string; - content: { - url: string; - info?: { - h?: number; - w?: number; - mimetype?: string; - size?: number; - thumbnail_info?: { - // eslint-disable-line camelcase - h?: number; - w?: number; - mimetype?: string; - size?: number; - }; - }; - }; + name: string + description?: string + content: { + url: string + info?: { + h?: number + w?: number + mimetype?: string + size?: number + thumbnail_info?: { + // eslint-disable-line camelcase + h?: number + w?: number + mimetype?: string + size?: number + } + } + } } export interface IStickerActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendSticker; - data: IStickerActionRequestData; + action: WidgetApiFromWidgetAction.SendSticker + data: IStickerActionRequestData } export interface IStickerActionResponse extends IStickerActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/StickyAction.ts b/src/interfaces/StickyAction.ts index a9726b8..31d4085 100644 --- a/src/interfaces/StickyAction.ts +++ b/src/interfaces/StickyAction.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" export interface IStickyActionRequestData extends IWidgetApiRequestData { - value: boolean; + value: boolean } export interface IStickyActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen; - data: IStickyActionRequestData; + action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen + data: IStickyActionRequestData } export interface IStickyActionResponseData extends IWidgetApiResponseData { - success: boolean; + success: boolean } export interface IStickyActionResponse extends IStickyActionRequest { - response: IStickyActionResponseData; + response: IStickyActionResponseData } diff --git a/src/interfaces/SupportedVersionsAction.ts b/src/interfaces/SupportedVersionsAction.ts index ea630e1..7e8ecc1 100644 --- a/src/interfaces/SupportedVersionsAction.ts +++ b/src/interfaces/SupportedVersionsAction.ts @@ -15,29 +15,29 @@ */ import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest"; + IWidgetApiRequest, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; -import { ApiVersion } from "./ApiVersion"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" +import { ApiVersion } from "./ApiVersion" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" export interface ISupportedVersionsActionRequest extends IWidgetApiRequest { - action: - | WidgetApiFromWidgetAction.SupportedApiVersions - | WidgetApiToWidgetAction.SupportedApiVersions; - data: IWidgetApiRequestEmptyData; + action: + | WidgetApiFromWidgetAction.SupportedApiVersions + | WidgetApiToWidgetAction.SupportedApiVersions + data: IWidgetApiRequestEmptyData } export interface ISupportedVersionsActionResponseData - extends IWidgetApiResponseData { - supported_versions: ApiVersion[]; // eslint-disable-line camelcase + extends IWidgetApiResponseData { + supported_versions: ApiVersion[] // eslint-disable-line camelcase } export interface ISupportedVersionsActionResponse - extends ISupportedVersionsActionRequest { - response: ISupportedVersionsActionResponseData; + extends ISupportedVersionsActionRequest { + response: ISupportedVersionsActionResponseData } diff --git a/src/interfaces/ThemeChangeAction.ts b/src/interfaces/ThemeChangeAction.ts index 9766e20..e781d76 100644 --- a/src/interfaces/ThemeChangeAction.ts +++ b/src/interfaces/ThemeChangeAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface IThemeChangeActionRequestData extends IWidgetApiRequestData { - // The format of a theme is deliberately unstandardized + // The format of a theme is deliberately unstandardized } export interface IThemeChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ThemeChange; - data: IThemeChangeActionRequestData; + action: WidgetApiToWidgetAction.ThemeChange + data: IThemeChangeActionRequestData } export interface IThemeChangeActionResponse extends IThemeChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/TurnServerActions.ts b/src/interfaces/TurnServerActions.ts index 2bed7f1..c89489a 100644 --- a/src/interfaces/TurnServerActions.ts +++ b/src/interfaces/TurnServerActions.ts @@ -15,52 +15,52 @@ */ import { - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest"; + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiRequestEmptyData, +} from "./IWidgetApiRequest" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "./WidgetApiAction" import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse"; + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse" export interface ITurnServer { - uris: string[]; - username: string; - password: string; + uris: string[] + username: string + password: string } export interface IWatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.WatchTurnServers; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.WatchTurnServers + data: IWidgetApiRequestEmptyData } export interface IWatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } export interface IUnwatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UnwatchTurnServers; - data: IWidgetApiRequestEmptyData; + action: WidgetApiFromWidgetAction.UnwatchTurnServers + data: IWidgetApiRequestEmptyData } export interface IUnwatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } export interface IUpdateTurnServersRequestData - extends IWidgetApiRequestData, - ITurnServer {} + extends IWidgetApiRequestData, + ITurnServer {} export interface IUpdateTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateTurnServers; - data: IUpdateTurnServersRequestData; + action: WidgetApiToWidgetAction.UpdateTurnServers + data: IUpdateTurnServersRequestData } export interface IUpdateTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/UpdateDelayedEventAction.ts b/src/interfaces/UpdateDelayedEventAction.ts index 92ba659..80f770e 100644 --- a/src/interfaces/UpdateDelayedEventAction.ts +++ b/src/interfaces/UpdateDelayedEventAction.ts @@ -14,34 +14,34 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" export enum UpdateDelayedEventAction { - Cancel = "cancel", - Restart = "restart", - Send = "send", + Cancel = "cancel", + Restart = "restart", + Send = "send", } export interface IUpdateDelayedEventFromWidgetRequestData - extends IWidgetApiRequestData { - delay_id: string; // eslint-disable-line camelcase - action: UpdateDelayedEventAction; + extends IWidgetApiRequestData { + delay_id: string // eslint-disable-line camelcase + action: UpdateDelayedEventAction } export interface IUpdateDelayedEventFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent; - data: IUpdateDelayedEventFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent + data: IUpdateDelayedEventFromWidgetRequestData } export interface IUpdateDelayedEventFromWidgetResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface IUpdateDelayedEventFromWidgetActionResponse - extends IUpdateDelayedEventFromWidgetActionRequest { - response: IUpdateDelayedEventFromWidgetResponseData; + extends IUpdateDelayedEventFromWidgetActionRequest { + response: IUpdateDelayedEventFromWidgetResponseData } diff --git a/src/interfaces/UpdateStateAction.ts b/src/interfaces/UpdateStateAction.ts index 1bbdac9..798c0cb 100644 --- a/src/interfaces/UpdateStateAction.ts +++ b/src/interfaces/UpdateStateAction.ts @@ -14,26 +14,26 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { IRoomEvent } from "./IRoomEvent"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IRoomEvent } from "./IRoomEvent" export interface IUpdateStateToWidgetRequestData extends IWidgetApiRequestData { - state: IRoomEvent[]; + state: IRoomEvent[] } export interface IUpdateStateToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateState; - data: IUpdateStateToWidgetRequestData; + action: WidgetApiToWidgetAction.UpdateState + data: IUpdateStateToWidgetRequestData } export interface IUpdateStateToWidgetResponseData - extends IWidgetApiResponseData { - // nothing + extends IWidgetApiResponseData { + // nothing } export interface IUpdateStateToWidgetActionResponse - extends IUpdateStateToWidgetActionRequest { - response: IUpdateStateToWidgetResponseData; + extends IUpdateStateToWidgetActionRequest { + response: IUpdateStateToWidgetResponseData } diff --git a/src/interfaces/UploadFileAction.ts b/src/interfaces/UploadFileAction.ts index 86d529f..7bd2cf8 100644 --- a/src/interfaces/UploadFileAction.ts +++ b/src/interfaces/UploadFileAction.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" export interface IUploadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - file: XMLHttpRequestBodyInit; + extends IWidgetApiRequestData { + file: XMLHttpRequestBodyInit } export interface IUploadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; - data: IUploadFileActionFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction + data: IUploadFileActionFromWidgetRequestData } export interface IUploadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - content_uri: string; // eslint-disable-line camelcase + extends IWidgetApiResponseData { + content_uri: string // eslint-disable-line camelcase } export interface IUploadFileActionFromWidgetActionResponse - extends IUploadFileActionFromWidgetActionRequest { - response: IUploadFileActionFromWidgetResponseData; + extends IUploadFileActionFromWidgetActionRequest { + response: IUploadFileActionFromWidgetResponseData } diff --git a/src/interfaces/UserDirectorySearchAction.ts b/src/interfaces/UserDirectorySearchAction.ts index 9747818..a46a7b8 100644 --- a/src/interfaces/UserDirectorySearchAction.ts +++ b/src/interfaces/UserDirectorySearchAction.ts @@ -14,33 +14,33 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { IWidgetApiResponseData } from "./IWidgetApiResponse"; -import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { WidgetApiFromWidgetAction } from "./WidgetApiAction" export interface IUserDirectorySearchFromWidgetRequestData - extends IWidgetApiRequestData { - search_term: string; // eslint-disable-line camelcase - limit?: number; + extends IWidgetApiRequestData { + search_term: string // eslint-disable-line camelcase + limit?: number } export interface IUserDirectorySearchFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch; - data: IUserDirectorySearchFromWidgetRequestData; + extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch + data: IUserDirectorySearchFromWidgetRequestData } export interface IUserDirectorySearchFromWidgetResponseData - extends IWidgetApiResponseData { - limited: boolean; - results: Array<{ - user_id: string; // eslint-disable-line camelcase - display_name?: string; // eslint-disable-line camelcase - avatar_url?: string; // eslint-disable-line camelcase - }>; + extends IWidgetApiResponseData { + limited: boolean + results: Array<{ + user_id: string // eslint-disable-line camelcase + display_name?: string // eslint-disable-line camelcase + avatar_url?: string // eslint-disable-line camelcase + }> } export interface IUserDirectorySearchFromWidgetActionResponse - extends IUserDirectorySearchFromWidgetActionRequest { - response: IUserDirectorySearchFromWidgetResponseData; + extends IUserDirectorySearchFromWidgetActionRequest { + response: IUserDirectorySearchFromWidgetResponseData } diff --git a/src/interfaces/VisibilityAction.ts b/src/interfaces/VisibilityAction.ts index fdb6454..f3f9479 100644 --- a/src/interfaces/VisibilityAction.ts +++ b/src/interfaces/VisibilityAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" export interface IVisibilityActionRequestData extends IWidgetApiRequestData { - visible: boolean; + visible: boolean } export interface IVisibilityActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateVisibility; - data: IVisibilityActionRequestData; + action: WidgetApiToWidgetAction.UpdateVisibility + data: IVisibilityActionRequestData } export interface IVisibilityActionResponse extends IVisibilityActionRequest { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index 71e12f8..174059e 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -15,86 +15,86 @@ */ export enum WidgetApiToWidgetAction { - SupportedApiVersions = "supported_api_versions", - Capabilities = "capabilities", - NotifyCapabilities = "notify_capabilities", - ThemeChange = "theme_change", - LanguageChange = "language_change", - TakeScreenshot = "screenshot", - UpdateVisibility = "visibility", - OpenIDCredentials = "openid_credentials", - WidgetConfig = "widget_config", - CloseModalWidget = "close_modal", - ButtonClicked = "button_clicked", - SendEvent = "send_event", - SendToDevice = "send_to_device", - UpdateState = "update_state", - UpdateTurnServers = "update_turn_servers", + SupportedApiVersions = "supported_api_versions", + Capabilities = "capabilities", + NotifyCapabilities = "notify_capabilities", + ThemeChange = "theme_change", + LanguageChange = "language_change", + TakeScreenshot = "screenshot", + UpdateVisibility = "visibility", + OpenIDCredentials = "openid_credentials", + WidgetConfig = "widget_config", + CloseModalWidget = "close_modal", + ButtonClicked = "button_clicked", + SendEvent = "send_event", + SendToDevice = "send_to_device", + UpdateState = "update_state", + UpdateTurnServers = "update_turn_servers", } export enum WidgetApiFromWidgetAction { - SupportedApiVersions = "supported_api_versions", - ContentLoaded = "content_loaded", - SendSticker = "m.sticker", - UpdateAlwaysOnScreen = "set_always_on_screen", - GetOpenIDCredentials = "get_openid", - CloseModalWidget = "close_modal", - OpenModalWidget = "open_modal", - SetModalButtonEnabled = "set_button_enabled", - SendEvent = "send_event", - SendToDevice = "send_to_device", - WatchTurnServers = "watch_turn_servers", - UnwatchTurnServers = "unwatch_turn_servers", + SupportedApiVersions = "supported_api_versions", + ContentLoaded = "content_loaded", + SendSticker = "m.sticker", + UpdateAlwaysOnScreen = "set_always_on_screen", + GetOpenIDCredentials = "get_openid", + CloseModalWidget = "close_modal", + OpenModalWidget = "open_modal", + SetModalButtonEnabled = "set_button_enabled", + SendEvent = "send_event", + SendToDevice = "send_to_device", + WatchTurnServers = "watch_turn_servers", + UnwatchTurnServers = "unwatch_turn_servers", - BeeperReadRoomAccountData = "com.beeper.read_room_account_data", + BeeperReadRoomAccountData = "com.beeper.read_room_account_data", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2876ReadEvents = "org.matrix.msc2876.read_events", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2876ReadEvents = "org.matrix.msc2876.read_events", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2931Navigate = "org.matrix.msc2931.navigate", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2931Navigate = "org.matrix.msc2931.navigate", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC2974RenegotiateCapabilities = "org.matrix.msc2974.request_capabilities", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3869ReadRelations = "org.matrix.msc3869.read_relations", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3869ReadRelations = "org.matrix.msc3869.read_relations", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC3973UserDirectorySearch = "org.matrix.msc3973.user_directory_search", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039GetMediaConfigAction = "org.matrix.msc4039.get_media_config", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039UploadFileAction = "org.matrix.msc4039.upload_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4039DownloadFileAction = "org.matrix.msc4039.download_file", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4039DownloadFileAction = "org.matrix.msc4039.download_file", - /** - * @deprecated It is not recommended to rely on this existing - it can be removed without notice. - */ - MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", + /** + * @deprecated It is not recommended to rely on this existing - it can be removed without notice. + */ + MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } export type WidgetApiAction = - | WidgetApiToWidgetAction - | WidgetApiFromWidgetAction - | string; + | WidgetApiToWidgetAction + | WidgetApiFromWidgetAction + | string diff --git a/src/interfaces/WidgetApiDirection.ts b/src/interfaces/WidgetApiDirection.ts index 6f9b875..a4f68b7 100644 --- a/src/interfaces/WidgetApiDirection.ts +++ b/src/interfaces/WidgetApiDirection.ts @@ -15,16 +15,16 @@ */ export enum WidgetApiDirection { - ToWidget = "toWidget", - FromWidget = "fromWidget", + ToWidget = "toWidget", + FromWidget = "fromWidget", } export function invertedDirection(dir: WidgetApiDirection): WidgetApiDirection { - if (dir === WidgetApiDirection.ToWidget) { - return WidgetApiDirection.FromWidget; - } else if (dir === WidgetApiDirection.FromWidget) { - return WidgetApiDirection.ToWidget; - } else { - throw new Error("Invalid direction"); - } + if (dir === WidgetApiDirection.ToWidget) { + return WidgetApiDirection.FromWidget + } else if (dir === WidgetApiDirection.FromWidget) { + return WidgetApiDirection.ToWidget + } else { + throw new Error("Invalid direction") + } } diff --git a/src/interfaces/WidgetConfigAction.ts b/src/interfaces/WidgetConfigAction.ts index 4989a7b..59f5d7c 100644 --- a/src/interfaces/WidgetConfigAction.ts +++ b/src/interfaces/WidgetConfigAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest } from "./IWidgetApiRequest"; -import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiRequest } from "./IWidgetApiRequest" +import { WidgetApiToWidgetAction } from "./WidgetApiAction" import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse"; -import { IModalWidgetOpenRequestData } from "./ModalWidgetActions"; + IWidgetApiAcknowledgeResponseData, + IWidgetApiResponse, +} from "./IWidgetApiResponse" +import { IModalWidgetOpenRequestData } from "./ModalWidgetActions" export interface IWidgetConfigRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.WidgetConfig; - data: IModalWidgetOpenRequestData; + action: WidgetApiToWidgetAction.WidgetConfig + data: IModalWidgetOpenRequestData } export interface IWidgetConfigResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData; + response: IWidgetApiAcknowledgeResponseData } diff --git a/src/interfaces/WidgetKind.ts b/src/interfaces/WidgetKind.ts index 8c79b22..374e198 100644 --- a/src/interfaces/WidgetKind.ts +++ b/src/interfaces/WidgetKind.ts @@ -15,7 +15,7 @@ */ export enum WidgetKind { - Room = "room", - Account = "account", - Modal = "modal", + Room = "room", + Account = "account", + Modal = "modal", } diff --git a/src/interfaces/WidgetType.ts b/src/interfaces/WidgetType.ts index 38ad5c4..12ce681 100644 --- a/src/interfaces/WidgetType.ts +++ b/src/interfaces/WidgetType.ts @@ -15,9 +15,9 @@ */ export enum MatrixWidgetType { - Custom = "m.custom", - JitsiMeet = "m.jitsi", - Stickerpicker = "m.stickerpicker", + Custom = "m.custom", + JitsiMeet = "m.jitsi", + Stickerpicker = "m.stickerpicker", } -export type WidgetType = MatrixWidgetType | string; +export type WidgetType = MatrixWidgetType | string diff --git a/src/models/Widget.ts b/src/models/Widget.ts index d1f340c..cc4dcf3 100644 --- a/src/models/Widget.ts +++ b/src/models/Widget.ts @@ -14,96 +14,96 @@ * limitations under the License. */ -import { IWidget, IWidgetData, WidgetType } from ".."; -import { assertPresent } from "./validation/utils"; -import { ITemplateParams, runTemplate } from ".."; +import { IWidget, IWidgetData, WidgetType } from ".." +import { assertPresent } from "./validation/utils" +import { ITemplateParams, runTemplate } from ".." /** * Represents the barest form of widget. */ export class Widget { - public constructor(private definition: IWidget) { - if (!this.definition) throw new Error("Definition is required"); + public constructor(private definition: IWidget) { + if (!this.definition) throw new Error("Definition is required") - assertPresent(definition, "id"); - assertPresent(definition, "creatorUserId"); - assertPresent(definition, "type"); - assertPresent(definition, "url"); - } + assertPresent(definition, "id") + assertPresent(definition, "creatorUserId") + assertPresent(definition, "type") + assertPresent(definition, "url") + } - /** - * The user ID who created the widget. - */ - public get creatorUserId(): string { - return this.definition.creatorUserId; - } + /** + * The user ID who created the widget. + */ + public get creatorUserId(): string { + return this.definition.creatorUserId + } - /** - * The type of widget. - */ - public get type(): WidgetType { - return this.definition.type; - } + /** + * The type of widget. + */ + public get type(): WidgetType { + return this.definition.type + } - /** - * The ID of the widget. - */ - public get id(): string { - return this.definition.id; - } + /** + * The ID of the widget. + */ + public get id(): string { + return this.definition.id + } - /** - * The name of the widget, or null if not set. - */ - public get name(): string | null { - return this.definition.name || null; - } + /** + * The name of the widget, or null if not set. + */ + public get name(): string | null { + return this.definition.name || null + } - /** - * The title for the widget, or null if not set. - */ - public get title(): string | null { - return this.rawData.title || null; - } + /** + * The title for the widget, or null if not set. + */ + public get title(): string | null { + return this.rawData.title || null + } - /** - * The templated URL for the widget. - */ - public get templateUrl(): string { - return this.definition.url; - } + /** + * The templated URL for the widget. + */ + public get templateUrl(): string { + return this.definition.url + } - /** - * The origin for this widget. - */ - public get origin(): string { - return new URL(this.templateUrl).origin; - } + /** + * The origin for this widget. + */ + public get origin(): string { + return new URL(this.templateUrl).origin + } - /** - * Whether or not the client should wait for the iframe to load. Defaults - * to true. - */ - public get waitForIframeLoad(): boolean { - if (this.definition.waitForIframeLoad === false) return false; - if (this.definition.waitForIframeLoad === true) return true; - return true; // default true - } + /** + * Whether or not the client should wait for the iframe to load. Defaults + * to true. + */ + public get waitForIframeLoad(): boolean { + if (this.definition.waitForIframeLoad === false) return false + if (this.definition.waitForIframeLoad === true) return true + return true // default true + } - /** - * The raw data for the widget. This will always be defined, though - * may be empty. - */ - public get rawData(): IWidgetData { - return this.definition.data || {}; - } + /** + * The raw data for the widget. This will always be defined, though + * may be empty. + */ + public get rawData(): IWidgetData { + return this.definition.data || {} + } - /** - * Gets a complete widget URL for the client to render. - * @param {ITemplateParams} params The template parameters. - * @returns {string} A templated URL. - */ - public getCompleteUrl(params: ITemplateParams): string { - return runTemplate(this.templateUrl, this.definition, params); - } + /** + * Gets a complete widget URL for the client to render. + * @param {ITemplateParams} params The template parameters. + * @returns {string} A templated URL. + */ + public getCompleteUrl(params: ITemplateParams): string { + return runTemplate(this.templateUrl, this.definition, params) + } } diff --git a/src/models/WidgetEventCapability.ts b/src/models/WidgetEventCapability.ts index 8655180..c9bb9a4 100644 --- a/src/models/WidgetEventCapability.ts +++ b/src/models/WidgetEventCapability.ts @@ -14,258 +14,279 @@ * limitations under the License. */ -import { Capability } from ".."; +import { Capability } from ".." export enum EventKind { - Event = "event", - State = "state_event", - ToDevice = "to_device", - RoomAccount = "room_account", + Event = "event", + State = "state_event", + ToDevice = "to_device", + RoomAccount = "room_account", } export enum EventDirection { - Send = "send", - Receive = "receive", + Send = "send", + Receive = "receive", } export class WidgetEventCapability { - private constructor( - public readonly direction: EventDirection, - public readonly eventType: string, - public readonly kind: EventKind, - public readonly keyStr: string | null, - public readonly raw: string, - ) {} - - public matchesAsStateEvent( - direction: EventDirection, - eventType: string, - stateKey: string | null, - ): boolean { - if (this.kind !== EventKind.State) return false; // not a state event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - if (this.keyStr === null) return true; // all state keys are allowed - if (this.keyStr === stateKey) return true; // this state key is allowed - - // Default not allowed - return false; - } - - public matchesAsToDeviceEvent( - direction: EventDirection, - eventType: string, - ): boolean { - if (this.kind !== EventKind.ToDevice) return false; // not a to-device event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - // Checks passed, the event is allowed - return true; - } - - public matchesAsRoomEvent( - direction: EventDirection, - eventType: string, - msgtype: string | null = null, - ): boolean { - if (this.kind !== EventKind.Event) return false; // not a room event - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - if (this.eventType === "m.room.message") { - if (this.keyStr === null) return true; // all message types are allowed - if (this.keyStr === msgtype) return true; // this message type is allowed - } else { - return true; // already passed the check for if the event is allowed + private constructor( + public readonly direction: EventDirection, + public readonly eventType: string, + public readonly kind: EventKind, + public readonly keyStr: string | null, + public readonly raw: string, + ) {} + + public matchesAsStateEvent( + direction: EventDirection, + eventType: string, + stateKey: string | null, + ): boolean { + if (this.kind !== EventKind.State) return false // not a state event + if (this.direction !== direction) return false // direction mismatch + if (this.eventType !== eventType) return false // event type mismatch + if (this.keyStr === null) return true // all state keys are allowed + if (this.keyStr === stateKey) return true // this state key is allowed + + // Default not allowed + return false } - // Default not allowed - return false; - } - - public matchesAsRoomAccountData( - direction: EventDirection, - eventType: string, - ): boolean { - if (this.kind !== EventKind.RoomAccount) return false; // not room account data - if (this.direction !== direction) return false; // direction mismatch - if (this.eventType !== eventType) return false; // event type mismatch - - // Checks passed, the event is allowed - return true; - } - - public static forStateEvent( - direction: EventDirection, - eventType: string, - stateKey?: string, - ): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - eventType = eventType.replace(/#/g, "\\#"); - stateKey = - stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ""; - const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forToDeviceEvent( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/56 - const str = `org.matrix.msc3819.${direction}.to_device:${eventType}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomEvent( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - const str = `org.matrix.msc2762.${direction}.event:${eventType}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomMessageEvent( - direction: EventDirection, - msgtype?: string, - ): WidgetEventCapability { - // TODO: Enable support for m.* namespace once the MSC lands. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - msgtype = msgtype === null || msgtype === undefined ? "" : msgtype; - const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}`; - - // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - public static forRoomAccountData( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { - const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}`; - - return WidgetEventCapability.findEventCapabilities([str])[0]; - } - - /** - * Parses a capabilities request to find all the event capability requests. - * @param {Iterable} capabilities The capabilities requested/to parse. - * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. - */ - public static findEventCapabilities( - capabilities: Iterable, - ): WidgetEventCapability[] { - const parsed: WidgetEventCapability[] = []; - for (const cap of capabilities) { - let direction: EventDirection | null = null; - let eventSegment: string | undefined; - let kind: EventKind | null = null; - - // TODO: Enable support for m.* namespace once the MSCs land. - // https://github.com/matrix-org/matrix-widget-api/issues/22 - // https://github.com/matrix-org/matrix-widget-api/issues/56 - - if (cap.startsWith("org.matrix.msc2762.send.event:")) { - direction = EventDirection.Send; - kind = EventKind.Event; - eventSegment = cap.substring("org.matrix.msc2762.send.event:".length); - } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { - direction = EventDirection.Send; - kind = EventKind.State; - eventSegment = cap.substring( - "org.matrix.msc2762.send.state_event:".length, - ); - } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { - direction = EventDirection.Send; - kind = EventKind.ToDevice; - eventSegment = cap.substring( - "org.matrix.msc3819.send.to_device:".length, - ); - } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { - direction = EventDirection.Receive; - kind = EventKind.Event; - eventSegment = cap.substring( - "org.matrix.msc2762.receive.event:".length, - ); - } else if (cap.startsWith("org.matrix.msc2762.receive.state_event:")) { - direction = EventDirection.Receive; - kind = EventKind.State; - eventSegment = cap.substring( - "org.matrix.msc2762.receive.state_event:".length, - ); - } else if (cap.startsWith("org.matrix.msc3819.receive.to_device:")) { - direction = EventDirection.Receive; - kind = EventKind.ToDevice; - eventSegment = cap.substring( - "org.matrix.msc3819.receive.to_device:".length, - ); - } else if ( - cap.startsWith("com.beeper.capabilities.receive.room_account_data:") - ) { - direction = EventDirection.Receive; - kind = EventKind.RoomAccount; - eventSegment = cap.substring( - "com.beeper.capabilities.receive.room_account_data:".length, - ); - } - - if (direction === null || kind === null || eventSegment === undefined) - continue; - - // The capability uses `#` as a separator between event type and state key/msgtype, - // so we split on that. However, a # is also valid in either one of those so we - // join accordingly. - // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". - const expectingKeyStr = - eventSegment.startsWith("m.room.message#") || kind === EventKind.State; - let keyStr: string | null = null; - if (eventSegment.includes("#") && expectingKeyStr) { - // Dev note: regex is difficult to write, so instead the rules are manually written - // out. This is probably just as understandable as a boring regex though, so win-win? - - // Test cases: - // str eventSegment keyStr - // ------------------------------------------------------------- - // m.room.message# m.room.message - // m.room.message#test m.room.message test - // m.room.message\# m.room.message# test - // m.room.message##test m.room.message #test - // m.room.message\##test m.room.message# test - // m.room.message\\##test m.room.message\# test - // m.room.message\\###test m.room.message\# #test - - // First step: explode the string - const parts = eventSegment.split("#"); - - // To form the eventSegment, we'll keep finding parts of the exploded string until - // there's one that doesn't end with the escape character (\). We'll then join those - // segments together with the exploding character. We have to remember to consume the - // escape character as well. - const idx = parts.findIndex((p) => !p.endsWith("\\")); - eventSegment = parts - .slice(0, idx + 1) - .map((p) => (p.endsWith("\\") ? p.substring(0, p.length - 1) : p)) - .join("#"); - - // The keyStr is whatever is left over. - keyStr = parts.slice(idx + 1).join("#"); - } - - parsed.push( - new WidgetEventCapability(direction, eventSegment, kind, keyStr, cap), - ); + public matchesAsToDeviceEvent( + direction: EventDirection, + eventType: string, + ): boolean { + if (this.kind !== EventKind.ToDevice) return false // not a to-device event + if (this.direction !== direction) return false // direction mismatch + if (this.eventType !== eventType) return false // event type mismatch + + // Checks passed, the event is allowed + return true + } + + public matchesAsRoomEvent( + direction: EventDirection, + eventType: string, + msgtype: string | null = null, + ): boolean { + if (this.kind !== EventKind.Event) return false // not a room event + if (this.direction !== direction) return false // direction mismatch + if (this.eventType !== eventType) return false // event type mismatch + + if (this.eventType === "m.room.message") { + if (this.keyStr === null) return true // all message types are allowed + if (this.keyStr === msgtype) return true // this message type is allowed + } else { + return true // already passed the check for if the event is allowed + } + + // Default not allowed + return false + } + + public matchesAsRoomAccountData( + direction: EventDirection, + eventType: string, + ): boolean { + if (this.kind !== EventKind.RoomAccount) return false // not room account data + if (this.direction !== direction) return false // direction mismatch + if (this.eventType !== eventType) return false // event type mismatch + + // Checks passed, the event is allowed + return true + } + + public static forStateEvent( + direction: EventDirection, + eventType: string, + stateKey?: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + eventType = eventType.replace(/#/g, "\\#") + stateKey = + stateKey !== null && stateKey !== undefined ? `#${stateKey}` : "" + const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}` + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0] + } + + public static forToDeviceEvent( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/56 + const str = `org.matrix.msc3819.${direction}.to_device:${eventType}` + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0] + } + + public static forRoomEvent( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + const str = `org.matrix.msc2762.${direction}.event:${eventType}` + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0] + } + + public static forRoomMessageEvent( + direction: EventDirection, + msgtype?: string, + ): WidgetEventCapability { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + msgtype = msgtype === null || msgtype === undefined ? "" : msgtype + const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}` + + // cheat by sending it through the processor + return WidgetEventCapability.findEventCapabilities([str])[0] + } + + public static forRoomAccountData( + direction: EventDirection, + eventType: string, + ): WidgetEventCapability { + const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}` + + return WidgetEventCapability.findEventCapabilities([str])[0] + } + + /** + * Parses a capabilities request to find all the event capability requests. + * @param {Iterable} capabilities The capabilities requested/to parse. + * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. + */ + public static findEventCapabilities( + capabilities: Iterable, + ): WidgetEventCapability[] { + const parsed: WidgetEventCapability[] = [] + for (const cap of capabilities) { + let direction: EventDirection | null = null + let eventSegment: string | undefined + let kind: EventKind | null = null + + // TODO: Enable support for m.* namespace once the MSCs land. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + // https://github.com/matrix-org/matrix-widget-api/issues/56 + + if (cap.startsWith("org.matrix.msc2762.send.event:")) { + direction = EventDirection.Send + kind = EventKind.Event + eventSegment = cap.substring( + "org.matrix.msc2762.send.event:".length, + ) + } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { + direction = EventDirection.Send + kind = EventKind.State + eventSegment = cap.substring( + "org.matrix.msc2762.send.state_event:".length, + ) + } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { + direction = EventDirection.Send + kind = EventKind.ToDevice + eventSegment = cap.substring( + "org.matrix.msc3819.send.to_device:".length, + ) + } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { + direction = EventDirection.Receive + kind = EventKind.Event + eventSegment = cap.substring( + "org.matrix.msc2762.receive.event:".length, + ) + } else if ( + cap.startsWith("org.matrix.msc2762.receive.state_event:") + ) { + direction = EventDirection.Receive + kind = EventKind.State + eventSegment = cap.substring( + "org.matrix.msc2762.receive.state_event:".length, + ) + } else if ( + cap.startsWith("org.matrix.msc3819.receive.to_device:") + ) { + direction = EventDirection.Receive + kind = EventKind.ToDevice + eventSegment = cap.substring( + "org.matrix.msc3819.receive.to_device:".length, + ) + } else if ( + cap.startsWith( + "com.beeper.capabilities.receive.room_account_data:", + ) + ) { + direction = EventDirection.Receive + kind = EventKind.RoomAccount + eventSegment = cap.substring( + "com.beeper.capabilities.receive.room_account_data:".length, + ) + } + + if ( + direction === null || + kind === null || + eventSegment === undefined + ) + continue + + // The capability uses `#` as a separator between event type and state key/msgtype, + // so we split on that. However, a # is also valid in either one of those so we + // join accordingly. + // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". + const expectingKeyStr = + eventSegment.startsWith("m.room.message#") || + kind === EventKind.State + let keyStr: string | null = null + if (eventSegment.includes("#") && expectingKeyStr) { + // Dev note: regex is difficult to write, so instead the rules are manually written + // out. This is probably just as understandable as a boring regex though, so win-win? + + // Test cases: + // str eventSegment keyStr + // ------------------------------------------------------------- + // m.room.message# m.room.message + // m.room.message#test m.room.message test + // m.room.message\# m.room.message# test + // m.room.message##test m.room.message #test + // m.room.message\##test m.room.message# test + // m.room.message\\##test m.room.message\# test + // m.room.message\\###test m.room.message\# #test + + // First step: explode the string + const parts = eventSegment.split("#") + + // To form the eventSegment, we'll keep finding parts of the exploded string until + // there's one that doesn't end with the escape character (\). We'll then join those + // segments together with the exploding character. We have to remember to consume the + // escape character as well. + const idx = parts.findIndex((p) => !p.endsWith("\\")) + eventSegment = parts + .slice(0, idx + 1) + .map((p) => + p.endsWith("\\") ? p.substring(0, p.length - 1) : p, + ) + .join("#") + + // The keyStr is whatever is left over. + keyStr = parts.slice(idx + 1).join("#") + } + + parsed.push( + new WidgetEventCapability( + direction, + eventSegment, + kind, + keyStr, + cap, + ), + ) + } + return parsed } - return parsed; - } } diff --git a/src/models/WidgetParser.ts b/src/models/WidgetParser.ts index bf82365..90e5058 100644 --- a/src/models/WidgetParser.ts +++ b/src/models/WidgetParser.ts @@ -14,143 +14,143 @@ * limitations under the License. */ -import { Widget } from "./Widget"; -import { IWidget } from ".."; -import { isValidUrl } from "./validation/url"; +import { Widget } from "./Widget" +import { IWidget } from ".." +import { isValidUrl } from "./validation/url" export interface IStateEvent { - event_id: string; // eslint-disable-line camelcase - room_id: string; // eslint-disable-line camelcase - type: string; - sender: string; - origin_server_ts: number; // eslint-disable-line camelcase - unsigned?: unknown; - content: unknown; - state_key: string; // eslint-disable-line camelcase + event_id: string // eslint-disable-line camelcase + room_id: string // eslint-disable-line camelcase + type: string + sender: string + origin_server_ts: number // eslint-disable-line camelcase + unsigned?: unknown + content: unknown + state_key: string // eslint-disable-line camelcase } export interface IAccountDataWidgets { - [widgetId: string]: { - type: "m.widget"; - // the state_key is also the widget's ID - state_key: string; // eslint-disable-line camelcase - sender: string; // current user's ID - content: IWidget; - id?: string; // off-spec, but possible - }; + [widgetId: string]: { + type: "m.widget" + // the state_key is also the widget's ID + state_key: string // eslint-disable-line camelcase + sender: string // current user's ID + content: IWidget + id?: string // off-spec, but possible + } } export class WidgetParser { - private constructor() { - // private constructor because this is a util class - } - - /** - * Parses widgets from the "m.widgets" account data event. This will always - * return an array, though may be empty if no valid widgets were found. - * @param {IAccountDataWidgets} content The content of the "m.widgets" account data. - * @returns {Widget[]} The widgets in account data, or an empty array. - */ - public static parseAccountData(content: IAccountDataWidgets): Widget[] { - if (!content) return []; - - const result: Widget[] = []; - for (const widgetId of Object.keys(content)) { - const roughWidget = content[widgetId]; - if (!roughWidget) continue; - if ( - roughWidget.type !== "m.widget" && - roughWidget.type !== "im.vector.modular.widgets" - ) - continue; - if (!roughWidget.sender) continue; - - const probableWidgetId = roughWidget.state_key || roughWidget.id; - if (probableWidgetId !== widgetId) continue; - - const asStateEvent: IStateEvent = { - content: roughWidget.content, - sender: roughWidget.sender, - type: "m.widget", - state_key: widgetId, - event_id: "$example", - room_id: "!example", - origin_server_ts: 1, - }; - - const widget = WidgetParser.parseRoomWidget(asStateEvent); - if (widget) result.push(widget); + private constructor() { + // private constructor because this is a util class } - return result; - } - - /** - * Parses all the widgets possible in the given array. This will always return - * an array, though may be empty if no widgets could be parsed. - * @param {IStateEvent[]} currentState The room state to parse. - * @returns {Widget[]} The widgets in the state, or an empty array. - */ - public static parseWidgetsFromRoomState( - currentState: IStateEvent[], - ): Widget[] { - if (!currentState) return []; - const result: Widget[] = []; - for (const state of currentState) { - const widget = WidgetParser.parseRoomWidget(state); - if (widget) result.push(widget); + /** + * Parses widgets from the "m.widgets" account data event. This will always + * return an array, though may be empty if no valid widgets were found. + * @param {IAccountDataWidgets} content The content of the "m.widgets" account data. + * @returns {Widget[]} The widgets in account data, or an empty array. + */ + public static parseAccountData(content: IAccountDataWidgets): Widget[] { + if (!content) return [] + + const result: Widget[] = [] + for (const widgetId of Object.keys(content)) { + const roughWidget = content[widgetId] + if (!roughWidget) continue + if ( + roughWidget.type !== "m.widget" && + roughWidget.type !== "im.vector.modular.widgets" + ) + continue + if (!roughWidget.sender) continue + + const probableWidgetId = roughWidget.state_key || roughWidget.id + if (probableWidgetId !== widgetId) continue + + const asStateEvent: IStateEvent = { + content: roughWidget.content, + sender: roughWidget.sender, + type: "m.widget", + state_key: widgetId, + event_id: "$example", + room_id: "!example", + origin_server_ts: 1, + } + + const widget = WidgetParser.parseRoomWidget(asStateEvent) + if (widget) result.push(widget) + } + + return result } - return result; - } - - /** - * Parses a state event into a widget. If the state event does not represent - * a widget (wrong event type, invalid widget, etc) then null is returned. - * @param {IStateEvent} stateEvent The state event. - * @returns {Widget|null} The widget, or null if invalid - */ - public static parseRoomWidget(stateEvent: IStateEvent): Widget | null { - if (!stateEvent) return null; - - // TODO: [Legacy] Remove legacy support - if ( - stateEvent.type !== "m.widget" && - stateEvent.type !== "im.vector.modular.widgets" - ) { - return null; + + /** + * Parses all the widgets possible in the given array. This will always return + * an array, though may be empty if no widgets could be parsed. + * @param {IStateEvent[]} currentState The room state to parse. + * @returns {Widget[]} The widgets in the state, or an empty array. + */ + public static parseWidgetsFromRoomState( + currentState: IStateEvent[], + ): Widget[] { + if (!currentState) return [] + const result: Widget[] = [] + for (const state of currentState) { + const widget = WidgetParser.parseRoomWidget(state) + if (widget) result.push(widget) + } + return result } - // Dev note: Throughout this function we have null safety to ensure that - // if the caller did not supply something useful that we don't error. This - // is done against the requirements of the interface because not everyone - // will have an interface to validate against. - - const content = (stateEvent.content as IWidget) || {}; - - // Form our best approximation of a widget with the information we have - const estimatedWidget: IWidget = { - id: stateEvent.state_key, - creatorUserId: content["creatorUserId"] || stateEvent.sender, - name: content["name"], - type: content["type"], - url: content["url"], - waitForIframeLoad: content["waitForIframeLoad"], - data: content["data"], - }; - - // Finally, process that widget - return WidgetParser.processEstimatedWidget(estimatedWidget); - } - - private static processEstimatedWidget(widget: IWidget): Widget | null { - // Validate that the widget has the best chance of passing as a widget - if (!widget.id || !widget.creatorUserId || !widget.type) { - return null; + /** + * Parses a state event into a widget. If the state event does not represent + * a widget (wrong event type, invalid widget, etc) then null is returned. + * @param {IStateEvent} stateEvent The state event. + * @returns {Widget|null} The widget, or null if invalid + */ + public static parseRoomWidget(stateEvent: IStateEvent): Widget | null { + if (!stateEvent) return null + + // TODO: [Legacy] Remove legacy support + if ( + stateEvent.type !== "m.widget" && + stateEvent.type !== "im.vector.modular.widgets" + ) { + return null + } + + // Dev note: Throughout this function we have null safety to ensure that + // if the caller did not supply something useful that we don't error. This + // is done against the requirements of the interface because not everyone + // will have an interface to validate against. + + const content = (stateEvent.content as IWidget) || {} + + // Form our best approximation of a widget with the information we have + const estimatedWidget: IWidget = { + id: stateEvent.state_key, + creatorUserId: content["creatorUserId"] || stateEvent.sender, + name: content["name"], + type: content["type"], + url: content["url"], + waitForIframeLoad: content["waitForIframeLoad"], + data: content["data"], + } + + // Finally, process that widget + return WidgetParser.processEstimatedWidget(estimatedWidget) } - if (!isValidUrl(widget.url)) { - return null; + + private static processEstimatedWidget(widget: IWidget): Widget | null { + // Validate that the widget has the best chance of passing as a widget + if (!widget.id || !widget.creatorUserId || !widget.type) { + return null + } + if (!isValidUrl(widget.url)) { + return null + } + // TODO: Validate data for known widget types + return new Widget(widget) } - // TODO: Validate data for known widget types - return new Widget(widget); - } } diff --git a/src/models/validation/url.ts b/src/models/validation/url.ts index 4f0480a..7e2a43e 100644 --- a/src/models/validation/url.ts +++ b/src/models/validation/url.ts @@ -15,18 +15,18 @@ */ export function isValidUrl(val: string): boolean { - if (!val) return false; // easy: not valid if not present + if (!val) return false // easy: not valid if not present - try { - const parsed = new URL(val); - if (parsed.protocol !== "http" && parsed.protocol !== "https") { - return false; + try { + const parsed = new URL(val) + if (parsed.protocol !== "http" && parsed.protocol !== "https") { + return false + } + return true + } catch (e) { + if (e instanceof TypeError) { + return false + } + throw e } - return true; - } catch (e) { - if (e instanceof TypeError) { - return false; - } - throw e; - } } diff --git a/src/models/validation/utils.ts b/src/models/validation/utils.ts index 52efb16..0410a95 100644 --- a/src/models/validation/utils.ts +++ b/src/models/validation/utils.ts @@ -16,10 +16,10 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function assertPresent>( - obj: O, - key: keyof O, + obj: O, + key: keyof O, ): void { - if (!obj[key]) { - throw new Error(`${String(key)} is required`); - } + if (!obj[key]) { + throw new Error(`${String(key)} is required`) + } } diff --git a/src/templating/url-template.ts b/src/templating/url-template.ts index 2861149..8fb42cb 100644 --- a/src/templating/url-template.ts +++ b/src/templating/url-template.ts @@ -14,62 +14,65 @@ * limitations under the License. */ -import { IWidget } from ".."; +import { IWidget } from ".." export interface ITemplateParams { - widgetRoomId?: string; - currentUserId: string; - userDisplayName?: string; - userHttpAvatarUrl?: string; - clientId?: string; - clientTheme?: string; - clientLanguage?: string; - deviceId?: string; - baseUrl?: string; + widgetRoomId?: string + currentUserId: string + userDisplayName?: string + userHttpAvatarUrl?: string + clientId?: string + clientTheme?: string + clientLanguage?: string + deviceId?: string + baseUrl?: string } export function runTemplate( - url: string, - widget: IWidget, - params: ITemplateParams, + url: string, + widget: IWidget, + params: ITemplateParams, ): string { - // Always apply the supplied params over top of data to ensure the data can't lie about them. - const variables = Object.assign({}, widget.data, { - matrix_room_id: params.widgetRoomId || "", - matrix_user_id: params.currentUserId, - matrix_display_name: params.userDisplayName || params.currentUserId, - matrix_avatar_url: params.userHttpAvatarUrl || "", - matrix_widget_id: widget.id, + // Always apply the supplied params over top of data to ensure the data can't lie about them. + const variables = Object.assign({}, widget.data, { + matrix_room_id: params.widgetRoomId || "", + matrix_user_id: params.currentUserId, + matrix_display_name: params.userDisplayName || params.currentUserId, + matrix_avatar_url: params.userHttpAvatarUrl || "", + matrix_widget_id: widget.id, - // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) - "org.matrix.msc2873.client_id": params.clientId || "", - "org.matrix.msc2873.client_theme": params.clientTheme || "", - "org.matrix.msc2873.client_language": params.clientLanguage || "", + // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) + "org.matrix.msc2873.client_id": params.clientId || "", + "org.matrix.msc2873.client_theme": params.clientTheme || "", + "org.matrix.msc2873.client_language": params.clientLanguage || "", - // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) - "org.matrix.msc3819.matrix_device_id": params.deviceId || "", + // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/3819) + "org.matrix.msc3819.matrix_device_id": params.deviceId || "", - // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) - "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", - }); - let result = url; - for (const key of Object.keys(variables)) { - // Regex escape from https://stackoverflow.com/a/6969486/7037379 - const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - const rexp = new RegExp(pattern, "g"); + // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) + "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", + }) + let result = url + for (const key of Object.keys(variables)) { + // Regex escape from https://stackoverflow.com/a/6969486/7037379 + const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string + const rexp = new RegExp(pattern, "g") - // This is technically not what we're supposed to do for a couple of reasons: - // 1. We are assuming that there won't later be a $key match after we replace a variable. - // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). - result = result.replace(rexp, encodeURIComponent(toString(variables[key]))); - } - return result; + // This is technically not what we're supposed to do for a couple of reasons: + // 1. We are assuming that there won't later be a $key match after we replace a variable. + // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). + result = result.replace( + rexp, + encodeURIComponent(toString(variables[key])), + ) + } + return result } export function toString(a: unknown): string { - if (a === null || a === undefined) { - return `${a}`; - } - // eslint-disable-next-line @typescript-eslint/no-base-to-string - return String(a); + if (a === null || a === undefined) { + return `${a}` + } + // eslint-disable-next-line @typescript-eslint/no-base-to-string + return String(a) } diff --git a/src/transport/ITransport.ts b/src/transport/ITransport.ts index b6dda14..e28fb63 100644 --- a/src/transport/ITransport.ts +++ b/src/transport/ITransport.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { EventEmitter } from "events"; +import { EventEmitter } from "events" import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - WidgetApiAction, -} from ".."; + IWidgetApiAcknowledgeResponseData, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + WidgetApiAction, +} from ".." /** * A transport for widget requests/responses. All actions @@ -31,85 +31,85 @@ import { * of the IWidgetApiRequest. */ export interface ITransport extends EventEmitter { - /** - * True if the transport is ready to start sending, false otherwise. - */ - readonly ready: boolean; + /** + * True if the transport is ready to start sending, false otherwise. + */ + readonly ready: boolean - /** - * The widget ID, if known. If not known, null. - */ - readonly widgetId: string | null; + /** + * The widget ID, if known. If not known, null. + */ + readonly widgetId: string | null - /** - * If true, the transport will refuse requests from origins other than the - * widget's current origin. This is intended to be used only by widgets which - * need excess security. - */ - strictOriginCheck: boolean; + /** + * If true, the transport will refuse requests from origins other than the + * widget's current origin. This is intended to be used only by widgets which + * need excess security. + */ + strictOriginCheck: boolean - /** - * The origin the transport should be replying/sending to. If not known, leave - * null. - */ - targetOrigin: string | null; + /** + * The origin the transport should be replying/sending to. If not known, leave + * null. + */ + targetOrigin: string | null - /** - * The number of seconds an outbound request is allowed to take before it - * times out. - */ - timeoutSeconds: number; + /** + * The number of seconds an outbound request is allowed to take before it + * times out. + */ + timeoutSeconds: number - /** - * Starts the transport for listening - */ - start(): void; + /** + * Starts the transport for listening + */ + start(): void - /** - * Stops the transport. It cannot be re-started. - */ - stop(): void; + /** + * Stops the transport. It cannot be re-started. + */ + stop(): void - /** - * Sends a request to the remote end. - * @param action The action to send. - * @param data The request data. - * @returns A promise which resolves to the remote end's response. - * @throws {Error} if the request failed with a generic error. - * @throws {WidgetApiResponseError} if the request failed with error details - * that can be communicated to the Widget API. - */ - send< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData, - >( - action: WidgetApiAction, - data: T, - ): Promise; + /** + * Sends a request to the remote end. + * @param action The action to send. + * @param data The request data. + * @returns A promise which resolves to the remote end's response. + * @throws {Error} if the request failed with a generic error. + * @throws {WidgetApiResponseError} if the request failed with error details + * that can be communicated to the Widget API. + */ + send< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData, + >( + action: WidgetApiAction, + data: T, + ): Promise - /** - * Sends a request to the remote end. This is similar to the send() function - * however this version returns the full response rather than just the response - * data. - * @param {WidgetApiAction} action The action to send. - * @param {IWidgetApiRequestData} data The request data. - * @returns {Promise} A promise which resolves to the remote end's response - * @throws {Error} if the request failed with a generic error. - * @throws {WidgetApiResponseError} if the request failed with error details - * that can be communicated to the Widget API. - */ - sendComplete( - action: WidgetApiAction, - data: T, - ): Promise; + /** + * Sends a request to the remote end. This is similar to the send() function + * however this version returns the full response rather than just the response + * data. + * @param {WidgetApiAction} action The action to send. + * @param {IWidgetApiRequestData} data The request data. + * @returns {Promise} A promise which resolves to the remote end's response + * @throws {Error} if the request failed with a generic error. + * @throws {WidgetApiResponseError} if the request failed with error details + * that can be communicated to the Widget API. + */ + sendComplete( + action: WidgetApiAction, + data: T, + ): Promise - /** - * Replies to a request. - * @param {IWidgetApiRequest} request The request to reply to. - * @param {IWidgetApiResponseData} responseData The response data to reply with. - */ - reply( - request: IWidgetApiRequest, - responseData: T, - ): void; + /** + * Replies to a request. + * @param {IWidgetApiRequest} request The request to reply to. + * @param {IWidgetApiResponseData} responseData The response data to reply with. + */ + reply( + request: IWidgetApiRequest, + responseData: T, + ): void } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index 733825d..e453930 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -14,203 +14,206 @@ * limitations under the License. */ -import { EventEmitter } from "events"; +import { EventEmitter } from "events" -import { ITransport } from "./ITransport"; +import { ITransport } from "./ITransport" import { - invertedDirection, - isErrorResponse, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - WidgetApiResponseError, - WidgetApiAction, - WidgetApiDirection, - WidgetApiToWidgetAction, -} from ".."; + invertedDirection, + isErrorResponse, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + WidgetApiResponseError, + WidgetApiAction, + WidgetApiDirection, + WidgetApiToWidgetAction, +} from ".." interface IOutboundRequest { - request: IWidgetApiRequest; - resolve: (response: IWidgetApiResponse) => void; - reject: (err: Error) => void; + request: IWidgetApiRequest + resolve: (response: IWidgetApiResponse) => void + reject: (err: Error) => void } /** * Transport for the Widget API over postMessage. */ export class PostmessageTransport extends EventEmitter implements ITransport { - public strictOriginCheck = false; - public targetOrigin = "*"; - public timeoutSeconds = 10; - - private _ready = false; - private _widgetId: string | null = null; - private outboundRequests = new Map(); - private stopController = new AbortController(); - - public get ready(): boolean { - return this._ready; - } - - public get widgetId(): string | null { - return this._widgetId || null; - } - - public constructor( - private sendDirection: WidgetApiDirection, - private initialWidgetId: string | null, - private transportWindow: Window, - private inboundWindow: Window, - ) { - super(); - this._widgetId = initialWidgetId; - } - - private get nextRequestId(): string { - const idBase = `widgetapi-${Date.now()}`; - let index = 0; - let id = idBase; - while (this.outboundRequests.has(id)) { - id = `${idBase}-${index++}`; + public strictOriginCheck = false + public targetOrigin = "*" + public timeoutSeconds = 10 + + private _ready = false + private _widgetId: string | null = null + private outboundRequests = new Map() + private stopController = new AbortController() + + public get ready(): boolean { + return this._ready + } + + public get widgetId(): string | null { + return this._widgetId || null + } + + public constructor( + private sendDirection: WidgetApiDirection, + private initialWidgetId: string | null, + private transportWindow: Window, + private inboundWindow: Window, + ) { + super() + this._widgetId = initialWidgetId + } + + private get nextRequestId(): string { + const idBase = `widgetapi-${Date.now()}` + let index = 0 + let id = idBase + while (this.outboundRequests.has(id)) { + id = `${idBase}-${index++}` + } + + // reserve the ID + this.outboundRequests.set(id, null) + + return id } - // reserve the ID - this.outboundRequests.set(id, null); - - return id; - } - - private sendInternal(message: IWidgetApiRequest | IWidgetApiResponse): void { - console.log( - `[PostmessageTransport] Sending object to ${this.targetOrigin}: `, - message, - ); - this.transportWindow.postMessage(message, this.targetOrigin); - } - - public reply( - request: IWidgetApiRequest, - responseData: T, - ): void { - return this.sendInternal({ - ...request, - response: responseData, - }); - } - - public send< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponseData, - >(action: WidgetApiAction, data: T): Promise { - return this.sendComplete(action, data).then((r) => r.response); - } - - public sendComplete< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponse, - >(action: WidgetApiAction, data: T): Promise { - if (!this.ready || !this.widgetId) { - return Promise.reject(new Error("Not ready or unknown widget ID")); + private sendInternal( + message: IWidgetApiRequest | IWidgetApiResponse, + ): void { + console.log( + `[PostmessageTransport] Sending object to ${this.targetOrigin}: `, + message, + ) + this.transportWindow.postMessage(message, this.targetOrigin) } - const request: IWidgetApiRequest = { - api: this.sendDirection, - widgetId: this.widgetId, - requestId: this.nextRequestId, - action: action, - data: data, - }; - if (action === WidgetApiToWidgetAction.UpdateVisibility) { - request["visible"] = data["visible"]; + + public reply( + request: IWidgetApiRequest, + responseData: T, + ): void { + return this.sendInternal({ + ...request, + response: responseData, + }) + } + + public send< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponseData, + >(action: WidgetApiAction, data: T): Promise { + return this.sendComplete(action, data).then((r) => r.response) + } + + public sendComplete< + T extends IWidgetApiRequestData, + R extends IWidgetApiResponse, + >(action: WidgetApiAction, data: T): Promise { + if (!this.ready || !this.widgetId) { + return Promise.reject(new Error("Not ready or unknown widget ID")) + } + const request: IWidgetApiRequest = { + api: this.sendDirection, + widgetId: this.widgetId, + requestId: this.nextRequestId, + action: action, + data: data, + } + if (action === WidgetApiToWidgetAction.UpdateVisibility) { + request["visible"] = data["visible"] + } + return new Promise((prResolve, prReject) => { + const resolve = (response: IWidgetApiResponse): void => { + cleanUp() + prResolve(response) + } + const reject = (err: Error): void => { + cleanUp() + prReject(err) + } + + const timerId = setTimeout( + () => reject(new Error("Request timed out")), + (this.timeoutSeconds || 1) * 1000, + ) + + const onStop = (): void => reject(new Error("Transport stopped")) + this.stopController.signal.addEventListener("abort", onStop) + + const cleanUp = (): void => { + this.outboundRequests.delete(request.requestId) + clearTimeout(timerId) + this.stopController.signal.removeEventListener("abort", onStop) + } + + this.outboundRequests.set(request.requestId, { + request, + resolve, + reject, + }) + this.sendInternal(request) + }) } - return new Promise((prResolve, prReject) => { - const resolve = (response: IWidgetApiResponse): void => { - cleanUp(); - prResolve(response); - }; - const reject = (err: Error): void => { - cleanUp(); - prReject(err); - }; - - const timerId = setTimeout( - () => reject(new Error("Request timed out")), - (this.timeoutSeconds || 1) * 1000, - ); - - const onStop = (): void => reject(new Error("Transport stopped")); - this.stopController.signal.addEventListener("abort", onStop); - - const cleanUp = (): void => { - this.outboundRequests.delete(request.requestId); - clearTimeout(timerId); - this.stopController.signal.removeEventListener("abort", onStop); - }; - - this.outboundRequests.set(request.requestId, { - request, - resolve, - reject, - }); - this.sendInternal(request); - }); - } - - public start(): void { - this.inboundWindow.addEventListener("message", (ev: MessageEvent) => { - this.handleMessage(ev); - }); - this._ready = true; - } - - public stop(): void { - this._ready = false; - this.stopController.abort(); - } - - private handleMessage(ev: MessageEvent): void { - if (this.stopController.signal.aborted) return; - if (!ev.data) return; // invalid event - - if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin - - // treat the message as a response first, then downgrade to a request - const response = ev.data; - if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response - - if (!response.response) { - // it's a request - const request = response; - if (request.api !== invertedDirection(this.sendDirection)) return; // wrong direction - this.handleRequest(request); - } else { - // it's a response - if (response.api !== this.sendDirection) return; // wrong direction - this.handleResponse(response); + + public start(): void { + this.inboundWindow.addEventListener("message", (ev: MessageEvent) => { + this.handleMessage(ev) + }) + this._ready = true + } + + public stop(): void { + this._ready = false + this.stopController.abort() } - } - private handleRequest(request: IWidgetApiRequest): void { - if (this.widgetId) { - if (this.widgetId !== request.widgetId) return; // wrong widget - } else { - this._widgetId = request.widgetId; + private handleMessage(ev: MessageEvent): void { + if (this.stopController.signal.aborted) return + if (!ev.data) return // invalid event + + if (this.strictOriginCheck && ev.origin !== window.origin) return // bad origin + + // treat the message as a response first, then downgrade to a request + const response = ev.data + if (!response.action || !response.requestId || !response.widgetId) + return // invalid request/response + + if (!response.response) { + // it's a request + const request = response + if (request.api !== invertedDirection(this.sendDirection)) return // wrong direction + this.handleRequest(request) + } else { + // it's a response + if (response.api !== this.sendDirection) return // wrong direction + this.handleResponse(response) + } } - this.emit("message", new CustomEvent("message", { detail: request })); - } + private handleRequest(request: IWidgetApiRequest): void { + if (this.widgetId) { + if (this.widgetId !== request.widgetId) return // wrong widget + } else { + this._widgetId = request.widgetId + } + + this.emit("message", new CustomEvent("message", { detail: request })) + } - private handleResponse(response: IWidgetApiResponse): void { - if (response.widgetId !== this.widgetId) return; // wrong widget + private handleResponse(response: IWidgetApiResponse): void { + if (response.widgetId !== this.widgetId) return // wrong widget - const req = this.outboundRequests.get(response.requestId); - if (!req) return; // response to an unknown request + const req = this.outboundRequests.get(response.requestId) + if (!req) return // response to an unknown request - if (isErrorResponse(response.response)) { - const { message, ...data } = response.response.error; - req.reject(new WidgetApiResponseError(message, data)); - } else { - req.resolve(response); + if (isErrorResponse(response.response)) { + const { message, ...data } = response.response.error + req.reject(new WidgetApiResponseError(message, data)) + } else { + req.resolve(response) + } } - } } diff --git a/src/util/SimpleObservable.ts b/src/util/SimpleObservable.ts index cffa861..c61c510 100644 --- a/src/util/SimpleObservable.ts +++ b/src/util/SimpleObservable.ts @@ -14,26 +14,26 @@ * limitations under the License. */ -export type ObservableFunction = (val: T) => void; +export type ObservableFunction = (val: T) => void export class SimpleObservable { - private listeners: ObservableFunction[] = []; + private listeners: ObservableFunction[] = [] - public constructor(initialFn?: ObservableFunction) { - if (initialFn) this.listeners.push(initialFn); - } + public constructor(initialFn?: ObservableFunction) { + if (initialFn) this.listeners.push(initialFn) + } - public onUpdate(fn: ObservableFunction): void { - this.listeners.push(fn); - } + public onUpdate(fn: ObservableFunction): void { + this.listeners.push(fn) + } - public update(val: T): void { - for (const listener of this.listeners) { - listener(val); + public update(val: T): void { + for (const listener of this.listeners) { + listener(val) + } } - } - public close(): void { - this.listeners = []; // reset - } + public close(): void { + this.listeners = [] // reset + } } diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index dff644a..debad49 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -15,2699 +15,2767 @@ * limitations under the License. */ -import { waitFor } from "@testing-library/dom"; - -import { ClientWidgetApi } from "../src/ClientWidgetApi"; -import { WidgetDriver } from "../src/driver/WidgetDriver"; -import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; -import { Capability } from "../src/interfaces/Capabilities"; -import { IRoomEvent } from "../src/interfaces/IRoomEvent"; -import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; -import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; -import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; -import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; +import { waitFor } from "@testing-library/dom" + +import { ClientWidgetApi } from "../src/ClientWidgetApi" +import { WidgetDriver } from "../src/driver/WidgetDriver" +import { UnstableApiVersion } from "../src/interfaces/ApiVersion" +import { Capability } from "../src/interfaces/Capabilities" +import { IRoomEvent } from "../src/interfaces/IRoomEvent" +import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest" +import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction" +import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction" +import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction" import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "../src/interfaces/WidgetApiAction"; -import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; -import { Widget } from "../src/models/Widget"; -import { PostmessageTransport } from "../src/transport/PostmessageTransport"; + WidgetApiFromWidgetAction, + WidgetApiToWidgetAction, +} from "../src/interfaces/WidgetApiAction" +import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection" +import { Widget } from "../src/models/Widget" +import { PostmessageTransport } from "../src/transport/PostmessageTransport" import { - IDownloadFileActionFromWidgetActionRequest, - IGetOpenIDActionRequest, - IMatrixApiError, - INavigateActionRequest, - IReadEventFromWidgetActionRequest, - ISendEventFromWidgetActionRequest, - ISendToDeviceFromWidgetActionRequest, - IUpdateDelayedEventFromWidgetActionRequest, - IUploadFileActionFromWidgetActionRequest, - IWidgetApiErrorResponseDataDetails, - OpenIDRequestState, - SimpleObservable, - Symbols, - UpdateDelayedEventAction, -} from "../src"; -import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction"; -import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction"; - -jest.mock("../src/transport/PostmessageTransport"); + IDownloadFileActionFromWidgetActionRequest, + IGetOpenIDActionRequest, + IMatrixApiError, + INavigateActionRequest, + IReadEventFromWidgetActionRequest, + ISendEventFromWidgetActionRequest, + ISendToDeviceFromWidgetActionRequest, + IUpdateDelayedEventFromWidgetActionRequest, + IUploadFileActionFromWidgetActionRequest, + IWidgetApiErrorResponseDataDetails, + OpenIDRequestState, + SimpleObservable, + Symbols, + UpdateDelayedEventAction, +} from "../src" +import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction" +import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction" + +jest.mock("../src/transport/PostmessageTransport") afterEach(() => { - jest.resetAllMocks(); -}); + jest.resetAllMocks() +}) function createRoomEvent(event: Partial = {}): IRoomEvent { - return { - type: "m.room.message", - sender: "user-id", - content: {}, - origin_server_ts: 0, - event_id: "id-0", - room_id: "!room-id", - unsigned: {}, - ...event, - }; + return { + type: "m.room.message", + sender: "user-id", + content: {}, + origin_server_ts: 0, + event_id: "id-0", + room_id: "!room-id", + unsigned: {}, + ...event, + } } class CustomMatrixError extends Error { - public constructor( - message: string, - public readonly httpStatus: number, - public readonly name: string, - public readonly data: Record, - ) { - super(message); - } + public constructor( + message: string, + public readonly httpStatus: number, + public readonly name: string, + public readonly data: Record, + ) { + super(message) + } } function processCustomMatrixError( - e: unknown, + e: unknown, ): IWidgetApiErrorResponseDataDetails | undefined { - return e instanceof CustomMatrixError - ? { - matrix_api_error: { - http_status: e.httpStatus, - http_headers: {}, - url: "", - response: { - errcode: e.name, - error: e.message, - ...e.data, - }, - }, - } - : undefined; + return e instanceof CustomMatrixError + ? { + matrix_api_error: { + http_status: e.httpStatus, + http_headers: {}, + url: "", + response: { + errcode: e.name, + error: e.message, + ...e.data, + }, + }, + } + : undefined } describe("ClientWidgetApi", () => { - let capabilities: Capability[]; - let iframe: HTMLIFrameElement; - let driver: jest.Mocked; - let clientWidgetApi: ClientWidgetApi; - let transport: PostmessageTransport; - let emitEvent: Parameters["1"]; - - async function loadIframe(caps: Capability[] = []): Promise { - capabilities = caps; - - const ready = new Promise((resolve) => { - clientWidgetApi.once("ready", resolve); - }); - - iframe.dispatchEvent(new Event("load")); - - await ready; - } - - beforeEach(() => { - capabilities = []; - iframe = document.createElement("iframe"); - document.body.appendChild(iframe); - - driver = { - navigate: jest.fn(), - readRoomTimeline: jest.fn(), - readRoomState: jest.fn(() => Promise.resolve([])), - readEventRelations: jest.fn(), - sendEvent: jest.fn(), - sendDelayedEvent: jest.fn(), - updateDelayedEvent: jest.fn(), - sendToDevice: jest.fn(), - askOpenID: jest.fn(), - readRoomAccountData: jest.fn(), - validateCapabilities: jest.fn(), - searchUserDirectory: jest.fn(), - getMediaConfig: jest.fn(), - uploadFile: jest.fn(), - downloadFile: jest.fn(), - getKnownRooms: jest.fn(() => []), - processError: jest.fn(), - } as Partial as jest.Mocked; - - clientWidgetApi = new ClientWidgetApi( - new Widget({ - id: "test", - creatorUserId: "@alice:example.org", - type: "example", - url: "https://example.org", - }), - iframe, - driver, - ); - - [transport] = jest.mocked(PostmessageTransport).mock.instances; - emitEvent = jest.mocked(transport.on).mock.calls[0][1]; - - jest.mocked(transport.send).mockResolvedValue({}); - jest - .mocked(driver.validateCapabilities) - .mockImplementation(async () => new Set(capabilities)); - }); - - afterEach(() => { - clientWidgetApi.stop(); - iframe.remove(); - }); - - it("should initiate capabilities", async () => { - await loadIframe(["m.always_on_screen"]); - - expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true); - expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false); - }); - - describe("navigate action", () => { - it("navigates", async () => { - driver.navigate.mockResolvedValue(Promise.resolve()); - - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; - - await loadIframe(["org.matrix.msc2931.navigate"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); - - expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); - }); - - it("fails to navigate", async () => { - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - }); - - expect(driver.navigate).not.toBeCalled(); - }); - - it("fails to navigate to an unsupported URI", async () => { - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://example.net", - }, - }; - - await loadIframe(["org.matrix.msc2931.navigate"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid matrix.to URI" }, - }); - }); - - expect(driver.navigate).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.navigate.mockRejectedValue(new Error("M_UNKNOWN: Unknown error")); - - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; - - await loadIframe(["org.matrix.msc2931.navigate"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error handling navigation" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.navigate.mockRejectedValue( - new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { - reason: "Unknown error", - }), - ); - - const event: INavigateActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2931Navigate, - data: { - uri: "https://matrix.to/#/#room:example.net", - }, - }; - - await loadIframe(["org.matrix.msc2931.navigate"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error handling navigation", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "failed to navigate", - reason: "Unknown error", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("send_event action", () => { - it("sends message events", async () => { - const roomId = "!room:example.org"; - const eventId = "$event:example.org"; - - driver.sendEvent.mockResolvedValue({ - roomId, - eventId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - event_id: eventId, - }); - }); - - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - null, - roomId, - ); - }); - - it("sends state events", async () => { - const roomId = "!room:example.org"; - const eventId = "$event:example.org"; - - driver.sendEvent.mockResolvedValue({ - roomId, - eventId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.topic", - content: {}, - state_key: "", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.state_event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - event_id: eventId, - }); - }); - - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - "", - roomId, - ); - }); - - it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org"; - - driver.sendEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org"; - - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendEvent.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { - reason: "Content must be a JSON object.", - }), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to send event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("send_event action for delayed events", () => { - it("fails to send delayed events", async () => { - const roomId = "!room:example.org"; - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - delay: 5000, - room_id: roomId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - // Without the required capability - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.sendDelayedEvent).not.toBeCalled(); - }); - - it("sends delayed message events", async () => { - const roomId = "!room:example.org"; - const parentDelayId = "fp"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: {}, - room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, - }); - }); - - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, - event.data.type, - event.data.content, - null, - roomId, - ); - }); - - it("sends delayed state events", async () => { - const roomId = "!room:example.org"; - const parentDelayId = "fp"; - const timeoutDelayId = "ft"; - - driver.sendDelayedEvent.mockResolvedValue({ - roomId, - delayId: timeoutDelayId, - }); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.topic", - content: {}, - state_key: "", - room_id: roomId, - delay: 5000, - parent_delay_id: parentDelayId, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.state_event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - room_id: roomId, - delay_id: timeoutDelayId, - }); - }); - - expect(driver.sendDelayedEvent).toHaveBeenCalledWith( - event.data.delay, - event.data.parent_delay_id, - event.data.type, - event.data.content, - "", - roomId, - ); - }); - - it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org"; - - driver.sendDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - delay: 5000, - parent_delay_id: "fp", - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org"; - - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendDelayedEvent.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { - reason: "Content must be a JSON object.", - }), - ); - - const event: ISendEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendEvent, - data: { - type: "m.room.message", - content: "hello", - room_id: roomId, - delay: 5000, - parent_delay_id: "fp", - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${event.data.room_id}`, - `org.matrix.msc2762.send.event:${event.data.type}`, - "org.matrix.msc4157.send.delayed_event", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to send event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("receiving events", () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - const event = createRoomEvent({ - room_id: roomId, - type: "m.room.message", - content: "hello", - }); - const eventFromOtherRoom = createRoomEvent({ - room_id: otherRoomId, - type: "m.room.message", - content: "test", - }); - - it("forwards events to the widget from one room only", async () => { - // Give the widget capabilities to receive from just one room - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); - - // Event from the matching room should be forwarded - clientWidgetApi.feedEvent(event); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ); - - // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ); - }); - - it("forwards events to the widget from the currently viewed room", async () => { - clientWidgetApi.setViewedRoomId(roomId); - // Give the widget capabilities to receive events without specifying - // any rooms that it can read - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); - - // Event from the viewed room should be forwarded - clientWidgetApi.feedEvent(event); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ); - - // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ); - - // View the other room; now the event can be forwarded - clientWidgetApi.setViewedRoomId(otherRoomId); - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ); - }); - - it("forwards events to the widget from all rooms", async () => { - // Give the widget capabilities to receive from any known room - await loadIframe([ - `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - "org.matrix.msc2762.receive.event:m.room.message", - ]); - - // Events from both rooms should be forwarded - clientWidgetApi.feedEvent(event); - clientWidgetApi.feedEvent(eventFromOtherRoom); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ); - }); - }); - - describe("receiving room state", () => { - it("syncs initial state and feeds updates", async () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - clientWidgetApi.setViewedRoomId(roomId); - const topicEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.topic", - state_key: "", - content: { topic: "Hello world!" }, - }); - const nameEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.name", - state_key: "", - content: { name: "Test room" }, - }); - const joinRulesEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.join_rules", - state_key: "", - content: { join_rule: "public" }, - }); - const otherRoomNameEvent = createRoomEvent({ - room_id: otherRoomId, - type: "m.room.name", - state_key: "", - content: { name: "Other room" }, - }); - - // Artificially delay the delivery of the join rules event - let resolveJoinRules: () => void; - const joinRules = new Promise( - (resolve) => (resolveJoinRules = resolve), - ); - - driver.readRoomState.mockImplementation( - async (rId, eventType, stateKey) => { - if (rId === roomId) { - if (eventType === "m.room.topic" && stateKey === "") - return [topicEvent]; - if (eventType === "m.room.name" && stateKey === "") - return [nameEvent]; - if (eventType === "m.room.join_rules" && stateKey === "") { - await joinRules; - return [joinRulesEvent]; - } - } else if (rId === otherRoomId) { - if (eventType === "m.room.name" && stateKey === "") - return [otherRoomNameEvent]; - } - return []; - }, - ); - - await loadIframe([ - "org.matrix.msc2762.receive.state_event:m.room.topic#", - "org.matrix.msc2762.receive.state_event:m.room.name#", - "org.matrix.msc2762.receive.state_event:m.room.join_rules#", - ]); - - // Simulate a race between reading the original join rules event and - // the join rules being updated at the same time - const newJoinRulesEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.join_rules", - state_key: "", - content: { join_rule: "invite" }, - }); - clientWidgetApi.feedStateUpdate(newJoinRulesEvent); - // What happens if the original join rules are delivered after the - // updated ones? - resolveJoinRules!(); - - await waitFor(() => { - // The initial topic and name should have been pushed - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: [topicEvent, nameEvent, newJoinRulesEvent], - }, - ); - // Only the updated join rules should have been delivered - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([joinRules]), - }, - ); - }); - - // Check that further updates to room state are pushed to the widget - // as expected - const newTopicEvent = createRoomEvent({ - room_id: roomId, - type: "m.room.topic", - state_key: "", - content: { topic: "Our new topic" }, - }); - clientWidgetApi.feedStateUpdate(newTopicEvent); - - await waitFor(() => { + let capabilities: Capability[] + let iframe: HTMLIFrameElement + let driver: jest.Mocked + let clientWidgetApi: ClientWidgetApi + let transport: PostmessageTransport + let emitEvent: Parameters["1"] + + async function loadIframe(caps: Capability[] = []): Promise { + capabilities = caps + + const ready = new Promise((resolve) => { + clientWidgetApi.once("ready", resolve) + }) + + iframe.dispatchEvent(new Event("load")) + + await ready + } + + beforeEach(() => { + capabilities = [] + iframe = document.createElement("iframe") + document.body.appendChild(iframe) + + driver = { + navigate: jest.fn(), + readRoomTimeline: jest.fn(), + readRoomState: jest.fn(() => Promise.resolve([])), + readEventRelations: jest.fn(), + sendEvent: jest.fn(), + sendDelayedEvent: jest.fn(), + updateDelayedEvent: jest.fn(), + sendToDevice: jest.fn(), + askOpenID: jest.fn(), + readRoomAccountData: jest.fn(), + validateCapabilities: jest.fn(), + searchUserDirectory: jest.fn(), + getMediaConfig: jest.fn(), + uploadFile: jest.fn(), + downloadFile: jest.fn(), + getKnownRooms: jest.fn(() => []), + processError: jest.fn(), + } as Partial as jest.Mocked + + clientWidgetApi = new ClientWidgetApi( + new Widget({ + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org", + }), + iframe, + driver, + ) + ;[transport] = jest.mocked(PostmessageTransport).mock.instances + emitEvent = jest.mocked(transport.on).mock.calls[0][1] + + jest.mocked(transport.send).mockResolvedValue({}) + jest.mocked(driver.validateCapabilities).mockImplementation( + async () => new Set(capabilities), + ) + }) + + afterEach(() => { + clientWidgetApi.stop() + iframe.remove() + }) + + it("should initiate capabilities", async () => { + await loadIframe(["m.always_on_screen"]) + + expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true) + expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false) + }) + + describe("navigate action", () => { + it("navigates", async () => { + driver.navigate.mockResolvedValue(Promise.resolve()) + + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + } + + await loadIframe(["org.matrix.msc2931.navigate"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}) + }) + + expect(driver.navigate).toHaveBeenCalledWith(event.data.uri) + }) + + it("fails to navigate", async () => { + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + } + + await loadIframe([]) // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }) + }) + + expect(driver.navigate).not.toBeCalled() + }) + + it("fails to navigate to an unsupported URI", async () => { + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://example.net", + }, + } + + await loadIframe(["org.matrix.msc2931.navigate"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid matrix.to URI" }, + }) + }) + + expect(driver.navigate).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.navigate.mockRejectedValue( + new Error("M_UNKNOWN: Unknown error"), + ) + + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + } + + await loadIframe(["org.matrix.msc2931.navigate"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error handling navigation" }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.navigate.mockRejectedValue( + new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { + reason: "Unknown error", + }), + ) + + const event: INavigateActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2931Navigate, + data: { + uri: "https://matrix.to/#/#room:example.net", + }, + } + + await loadIframe(["org.matrix.msc2931.navigate"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error handling navigation", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "failed to navigate", + reason: "Unknown error", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("send_event action", () => { + it("sends message events", async () => { + const roomId = "!room:example.org" + const eventId = "$event:example.org" + + driver.sendEvent.mockResolvedValue({ + roomId, + eventId, + }) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + room_id: roomId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + event_id: eventId, + }) + }) + + expect(driver.sendEvent).toHaveBeenCalledWith( + event.data.type, + event.data.content, + null, + roomId, + ) + }) + + it("sends state events", async () => { + const roomId = "!room:example.org" + const eventId = "$event:example.org" + + driver.sendEvent.mockResolvedValue({ + roomId, + eventId, + }) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.topic", + content: {}, + state_key: "", + room_id: roomId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.state_event:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + event_id: eventId, + }) + }) + + expect(driver.sendEvent).toHaveBeenCalledWith( + event.data.type, + event.data.content, + "", + roomId, + ) + }) + + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org" + + driver.sendEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org" + + driver.processError.mockImplementation(processCustomMatrixError) + + driver.sendEvent.mockRejectedValue( + new CustomMatrixError( + "failed to send event", + 400, + "M_NOT_JSON", + { + reason: "Content must be a JSON object.", + }, + ), + ) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("send_event action for delayed events", () => { + it("fails to send delayed events", async () => { + const roomId = "!room:example.org" + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + delay: 5000, + room_id: roomId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + // Without the required capability + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }) + }) + + expect(driver.sendDelayedEvent).not.toBeCalled() + }) + + it("sends delayed message events", async () => { + const roomId = "!room:example.org" + const parentDelayId = "fp" + const timeoutDelayId = "ft" + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: {}, + room_id: roomId, + delay: 5000, + parent_delay_id: parentDelayId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }) + }) + + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay, + event.data.parent_delay_id, + event.data.type, + event.data.content, + null, + roomId, + ) + }) + + it("sends delayed state events", async () => { + const roomId = "!room:example.org" + const parentDelayId = "fp" + const timeoutDelayId = "ft" + + driver.sendDelayedEvent.mockResolvedValue({ + roomId, + delayId: timeoutDelayId, + }) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.topic", + content: {}, + state_key: "", + room_id: roomId, + delay: 5000, + parent_delay_id: parentDelayId, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.state_event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + room_id: roomId, + delay_id: timeoutDelayId, + }) + }) + + expect(driver.sendDelayedEvent).toHaveBeenCalledWith( + event.data.delay, + event.data.parent_delay_id, + event.data.type, + event.data.content, + "", + roomId, + ) + }) + + it("should reject requests when the driver throws an exception", async () => { + const roomId = "!room:example.org" + + driver.sendDelayedEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + delay: 5000, + parent_delay_id: "fp", + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + const roomId = "!room:example.org" + + driver.processError.mockImplementation(processCustomMatrixError) + + driver.sendDelayedEvent.mockRejectedValue( + new CustomMatrixError( + "failed to send event", + 400, + "M_NOT_JSON", + { + reason: "Content must be a JSON object.", + }, + ), + ) + + const event: ISendEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendEvent, + data: { + type: "m.room.message", + content: "hello", + room_id: roomId, + delay: 5000, + parent_delay_id: "fp", + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${event.data.room_id}`, + `org.matrix.msc2762.send.event:${event.data.type}`, + "org.matrix.msc4157.send.delayed_event", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to send event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("receiving events", () => { + const roomId = "!room:example.org" + const otherRoomId = "!other-room:example.org" + const event = createRoomEvent({ + room_id: roomId, + type: "m.room.message", + content: "hello", + }) + const eventFromOtherRoom = createRoomEvent({ + room_id: otherRoomId, + type: "m.room.message", + content: "test", + }) + + it("forwards events to the widget from one room only", async () => { + // Give the widget capabilities to receive from just one room + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]) + + // Event from the matching room should be forwarded + clientWidgetApi.feedEvent(event) + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ) + + // Event from the other room should not be forwarded + clientWidgetApi.feedEvent(eventFromOtherRoom) + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ) + }) + + it("forwards events to the widget from the currently viewed room", async () => { + clientWidgetApi.setViewedRoomId(roomId) + // Give the widget capabilities to receive events without specifying + // any rooms that it can read + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]) + + // Event from the viewed room should be forwarded + clientWidgetApi.feedEvent(event) + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ) + + // Event from the other room should not be forwarded + clientWidgetApi.feedEvent(eventFromOtherRoom) + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ) + + // View the other room; now the event can be forwarded + clientWidgetApi.setViewedRoomId(otherRoomId) + clientWidgetApi.feedEvent(eventFromOtherRoom) + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ) + }) + + it("forwards events to the widget from all rooms", async () => { + // Give the widget capabilities to receive from any known room + await loadIframe([ + `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, + "org.matrix.msc2762.receive.event:m.room.message", + ]) + + // Events from both rooms should be forwarded + clientWidgetApi.feedEvent(event) + clientWidgetApi.feedEvent(eventFromOtherRoom) + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + event, + ) + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.SendEvent, + eventFromOtherRoom, + ) + }) + }) + + describe("receiving room state", () => { + it("syncs initial state and feeds updates", async () => { + const roomId = "!room:example.org" + const otherRoomId = "!other-room:example.org" + clientWidgetApi.setViewedRoomId(roomId) + const topicEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.topic", + state_key: "", + content: { topic: "Hello world!" }, + }) + const nameEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.name", + state_key: "", + content: { name: "Test room" }, + }) + const joinRulesEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "public" }, + }) + const otherRoomNameEvent = createRoomEvent({ + room_id: otherRoomId, + type: "m.room.name", + state_key: "", + content: { name: "Other room" }, + }) + + // Artificially delay the delivery of the join rules event + let resolveJoinRules: () => void + const joinRules = new Promise( + (resolve) => (resolveJoinRules = resolve), + ) + + driver.readRoomState.mockImplementation( + async (rId, eventType, stateKey) => { + if (rId === roomId) { + if (eventType === "m.room.topic" && stateKey === "") + return [topicEvent] + if (eventType === "m.room.name" && stateKey === "") + return [nameEvent] + if ( + eventType === "m.room.join_rules" && + stateKey === "" + ) { + await joinRules + return [joinRulesEvent] + } + } else if (rId === otherRoomId) { + if (eventType === "m.room.name" && stateKey === "") + return [otherRoomNameEvent] + } + return [] + }, + ) + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:m.room.topic#", + "org.matrix.msc2762.receive.state_event:m.room.name#", + "org.matrix.msc2762.receive.state_event:m.room.join_rules#", + ]) + + // Simulate a race between reading the original join rules event and + // the join rules being updated at the same time + const newJoinRulesEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.join_rules", + state_key: "", + content: { join_rule: "invite" }, + }) + clientWidgetApi.feedStateUpdate(newJoinRulesEvent) + // What happens if the original join rules are delivered after the + // updated ones? + resolveJoinRules!() + + await waitFor(() => { + // The initial topic and name should have been pushed + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: [topicEvent, nameEvent, newJoinRulesEvent], + }, + ) + // Only the updated join rules should have been delivered + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([joinRules]), + }, + ) + }) + + // Check that further updates to room state are pushed to the widget + // as expected + const newTopicEvent = createRoomEvent({ + room_id: roomId, + type: "m.room.topic", + state_key: "", + content: { topic: "Our new topic" }, + }) + clientWidgetApi.feedStateUpdate(newTopicEvent) + + await waitFor(() => { + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: [newTopicEvent], + }, + ) + }) + + // Up to this point we should not have received any state for the + // other (unviewed) room + expect(transport.send).not.toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([otherRoomNameEvent]), + }, + ) + // Now view the other room + clientWidgetApi.setViewedRoomId(otherRoomId) + ;(transport.send as unknown as jest.SpyInstance).mockClear() + + await waitFor(() => { + // The state of the other room should now be pushed + expect(transport.send).toHaveBeenCalledWith( + WidgetApiToWidgetAction.UpdateState, + { + state: expect.arrayContaining([otherRoomNameEvent]), + }, + ) + }) + }) + }) + + describe("update_delayed_event action", () => { + it("fails to update delayed events", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + } + + await loadIframe([]) // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }) + }) + + expect(driver.updateDelayedEvent).not.toBeCalled() + }) + + it("fails to update delayed events with unsupported action", async () => { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: "unknown" as UpdateDelayedEventAction, + }, + } + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }) + }) + + expect(driver.updateDelayedEvent).not.toBeCalled() + }) + + it("updates delayed events", async () => { + driver.updateDelayedEvent.mockResolvedValue(undefined) + + for (const action of [ + UpdateDelayedEventAction.Cancel, + UpdateDelayedEventAction.Restart, + UpdateDelayedEventAction.Send, + ]) { + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action, + }, + } + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}) + }) + + expect(driver.updateDelayedEvent).toHaveBeenCalledWith( + event.data.delay_id, + event.data.action, + ) + } + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.updateDelayedEvent.mockRejectedValue( + new Error("M_BAD_JSON: Content must be a JSON object"), + ) + + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + } + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error updating delayed event" }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.updateDelayedEvent.mockRejectedValue( + new CustomMatrixError( + "failed to update delayed event", + 400, + "M_NOT_JSON", + { + reason: "Content must be a JSON object.", + }, + ), + ) + + const event: IUpdateDelayedEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + data: { + delay_id: "f", + action: UpdateDelayedEventAction.Send, + }, + } + + await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error updating delayed event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_NOT_JSON", + error: "failed to update delayed event", + reason: "Content must be a JSON object.", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("send_to_device action", () => { + it("sends unencrypted to-device events", async () => { + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, {}) + }) + + expect(driver.sendToDevice).toHaveBeenCalledWith( + event.data.type, + event.data.encrypted, + event.data.messages, + ) + }) + + it("fails to send to-device events without event type", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing event type" }, + }) + }) + + expect(driver.sendToDevice).not.toBeCalled() + }) + + it("fails to send to-device events without event contents", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Invalid request - missing event contents", + }, + }) + }) + + expect(driver.sendToDevice).not.toBeCalled() + }) + + it("fails to send to-device events without encryption flag", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Invalid request - missing encryption flag", + }, + }) + }) + + expect(driver.sendToDevice).not.toBeCalled() + }) + + it("fails to send to-device events with any event type", async () => { + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}_different`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Cannot send to-device events of this type", + }, + }) + }) + + expect(driver.sendToDevice).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.sendToDevice.mockRejectedValue( + new Error( + "M_FORBIDDEN: You don't have permission to send to-device events", + ), + ) + + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Error sending event" }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.sendToDevice.mockRejectedValue( + new CustomMatrixError( + "failed to send event", + 400, + "M_FORBIDDEN", + { + reason: "You don't have permission to send to-device events", + }, + ), + ) + + const event: ISendToDeviceFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SendToDevice, + data: { + type: "net.example.test", + encrypted: false, + messages: { + "@foo:bar.com": { + DEVICEID: { + example_content_key: "value", + }, + }, + }, + }, + } + + await loadIframe([ + `org.matrix.msc3819.send.to_device:${event.data.type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Error sending event", + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_FORBIDDEN", + error: "failed to send event", + reason: "You don't have permission to send to-device events", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("get_openid action", () => { + it("gets info", async () => { + driver.askOpenID.mockImplementation((observable) => { + observable.update({ + state: OpenIDRequestState.Allowed, + token: { + access_token: "access_token", + }, + }) + }) + + const event: IGetOpenIDActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.GetOpenIDCredentials, + data: {}, + } + + await loadIframe([]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + state: OpenIDRequestState.Allowed, + access_token: "access_token", + }) + }) + + expect(driver.askOpenID).toHaveBeenCalledWith( + expect.any(SimpleObservable), + ) + }) + + it("fails when client provided invalid token", async () => { + driver.askOpenID.mockImplementation((observable) => { + observable.update({ + state: OpenIDRequestState.Allowed, + }) + }) + + const event: IGetOpenIDActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.GetOpenIDCredentials, + data: {}, + } + + await loadIframe([]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + error: { + message: + "client provided invalid OIDC token for an allowed request", + }, + }) + }) + + expect(driver.askOpenID).toHaveBeenCalledWith( + expect.any(SimpleObservable), + ) + }) + }) + + describe("com.beeper.read_room_account_data action", () => { + it("reads room account data", async () => { + const type = "net.example.test" + const roomId = "!room:example.org" + + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]) + + const event: IReadRoomAccountDataFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, + data: { + room_ids: [roomId], + type, + }, + } + + await loadIframe([ + `com.beeper.capabilities.receive.room_account_data:${type}`, + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + events: [ + { + type, + room_id: roomId, + content: {}, + }, + ], + }) + }) + + expect(driver.readRoomAccountData).toHaveBeenCalledWith( + event.data.type, + ) + }) + + it("does not read room account data", async () => { + const type = "net.example.test" + const roomId = "!room:example.org" + + driver.readRoomAccountData.mockResolvedValue([ + { + type, + room_id: roomId, + content: {}, + }, + ]) + + const event: IReadRoomAccountDataFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, + data: { + room_ids: [roomId], + type, + }, + } + + await loadIframe([]) // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + error: { + message: "Cannot read room account data of this type", + }, + }) + }) + + expect(driver.readRoomAccountData).toHaveBeenCalledWith( + event.data.type, + ) + }) + }) + + describe("org.matrix.msc2876.read_events action", () => { + it("reads events from a specific room", async () => { + const roomId = "!room:example.org" + const event = createRoomEvent({ + room_id: roomId, + type: "net.example.test", + content: "test", + }) + driver.readRoomTimeline.mockImplementation(async (rId) => { + if (rId === roomId) return [event] + return [] + }) + + const request: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + room_ids: [roomId], + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${roomId}`, + "org.matrix.msc2762.receive.event:net.example.test", + ]) + clientWidgetApi.setViewedRoomId(roomId) + + emitEvent(new CustomEvent("", { detail: request })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(request, { + events: [event], + }) + }) + + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + roomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ) + }) + + it("reads events from all rooms", async () => { + const roomId = "!room:example.org" + const otherRoomId = "!other-room:example.org" + const event = createRoomEvent({ + room_id: roomId, + type: "net.example.test", + content: "test", + }) + const otherRoomEvent = createRoomEvent({ + room_id: otherRoomId, + type: "net.example.test", + content: "hi", + }) + driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]) + driver.readRoomTimeline.mockImplementation(async (rId) => { + if (rId === roomId) return [event] + if (rId === otherRoomId) return [otherRoomEvent] + return [] + }) + + const request: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + room_ids: Symbols.AnyRoom, + }, + } + + await loadIframe([ + `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, + "org.matrix.msc2762.receive.event:net.example.test", + ]) + clientWidgetApi.setViewedRoomId(roomId) + + emitEvent(new CustomEvent("", { detail: request })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(request, { + events: [event, otherRoomEvent], + }) + }) + + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + roomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ) + expect(driver.readRoomTimeline).toHaveBeenCalledWith( + otherRoomId, + "net.example.test", + undefined, + undefined, + 0, + undefined, + ) + }) + + it("reads state events with any state key", async () => { + driver.readRoomTimeline.mockResolvedValue([ + createRoomEvent({ type: "net.example.test", state_key: "A" }), + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ]) + + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: true, + }, + } + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test", + ]) + clientWidgetApi.setViewedRoomId("!room-id") + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + events: [ + createRoomEvent({ + type: "net.example.test", + state_key: "A", + }), + createRoomEvent({ + type: "net.example.test", + state_key: "B", + }), + ], + }) + }) + + expect(driver.readRoomTimeline).toBeCalledWith( + "!room-id", + "net.example.test", + undefined, + undefined, + 0, + undefined, + ) + }) + + it("fails to read state events with any state key", async () => { + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: true, + }, + } + + await loadIframe([]) // Without the required capability + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }) + }) + + expect(driver.readRoomTimeline).not.toBeCalled() + }) + + it("reads state events with a specific state key", async () => { + driver.readRoomTimeline.mockResolvedValue([ + createRoomEvent({ type: "net.example.test", state_key: "B" }), + ]) + + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: "B", + }, + } + + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test#B", + ]) + clientWidgetApi.setViewedRoomId("!room-id") + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + events: [ + createRoomEvent({ + type: "net.example.test", + state_key: "B", + }), + ], + }) + }) + + expect(driver.readRoomTimeline).toBeCalledWith( + "!room-id", + "net.example.test", + undefined, + "B", + 0, + undefined, + ) + }) + + it("fails to read state events with a specific state key", async () => { + const event: IReadEventFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC2876ReadEvents, + data: { + type: "net.example.test", + state_key: "B", + }, + } + + // Request the capability for the wrong state key + await loadIframe([ + "org.matrix.msc2762.receive.state_event:net.example.test#A", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { message: expect.any(String) }, + }) + }) + + expect(driver.readRoomTimeline).not.toBeCalled() + }) + }) + + describe("org.matrix.msc3869.read_relations action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC3869, + ]), + }) + }) + + it("should handle and process the request", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [createRoomEvent()], + }) + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + } + + await loadIframe([ + "org.matrix.msc2762.receive.event:m.room.message", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [createRoomEvent()], + }) + }) + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ) + }) + + it("should only return events that match requested capabilities", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [ + createRoomEvent(), + createRoomEvent({ type: "m.reaction" }), + createRoomEvent({ + type: "net.example.test", + state_key: "A", + }), + createRoomEvent({ + type: "net.example.test", + state_key: "B", + }), + ], + }) + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + } + + await loadIframe([ + "org.matrix.msc2762.receive.event:m.room.message", + "org.matrix.msc2762.receive.state_event:net.example.test#A", + ]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [ + createRoomEvent(), + createRoomEvent({ + type: "net.example.test", + state_key: "A", + }), + ], + }) + }) + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ) + }) + + it("should accept all options and pass it to the driver", async () => { + driver.readEventRelations.mockResolvedValue({ + chunk: [], + }) + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!room-id", + event_type: "m.room.message", + rel_type: "m.reference", + limit: 25, + from: "from-token", + to: "to-token", + direction: "f", + }, + } + + await loadIframe(["org.matrix.msc2762.timeline:!room-id"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + chunk: [], + }) + }) + + expect(driver.readEventRelations).toBeCalledWith( + "$event", + "!room-id", + "m.reference", + "m.room.message", + "from-token", + "to-token", + 25, + "f", + ) + }) + + it("should reject requests without event_id", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing event ID" }, + }) + }) + + it("should reject requests with a negative limit", async () => { + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + limit: -1, + }, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - limit out of range" }, + }) + }) + + it("should reject requests when the room timeline was not requested", async () => { + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!another-room-id", + }, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unable to access room timeline: !another-room-id", + }, + }) + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.readEventRelations.mockRejectedValue( + new Error( + "M_FORBIDDEN: You don't have permission to access that event", + ), + ) + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + } + + await loadIframe() + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while reading relations", + }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.readEventRelations.mockRejectedValue( + new CustomMatrixError( + "failed to read relations", + 403, + "M_FORBIDDEN", + { + reason: "You don't have permission to access that event", + }, + ), + ) + + const event: IReadRelationsFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { event_id: "$event" }, + } + + await loadIframe() + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while reading relations", + matrix_api_error: { + http_status: 403, + http_headers: {}, + url: "", + response: { + errcode: "M_FORBIDDEN", + error: "failed to read relations", + reason: "You don't have permission to access that event", + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("org.matrix.msc3973.user_directory_search action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC3973, + ]), + }) + }) + + it("should handle and process the request", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: true, + results: [ + { + userId: "@foo:bar.com", + }, + ], + }) + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: true, + results: [ + { + user_id: "@foo:bar.com", + display_name: undefined, + avatar_url: undefined, + }, + ], + }) + }) + + expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined) + }) + + it("should accept all options and pass it to the driver", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: false, + results: [ + { + userId: "@foo:bar.com", + }, + { + userId: "@bar:foo.com", + displayName: "Bar", + avatarUrl: "mxc://...", + }, + ], + }) + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: 5, + }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: false, + results: [ + { + user_id: "@foo:bar.com", + display_name: undefined, + avatar_url: undefined, + }, + { + user_id: "@bar:foo.com", + display_name: "Bar", + avatar_url: "mxc://...", + }, + ], + }) + }) + + expect(driver.searchUserDirectory).toBeCalledWith("foo", 5) + }) + + it("should accept empty search_term", async () => { + driver.searchUserDirectory.mockResolvedValue({ + limited: false, + results: [], + }) + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "" }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + limited: false, + results: [], + }) + }) + + expect(driver.searchUserDirectory).toBeCalledWith("", undefined) + }) + + it("should reject requests when the capability was not requested", async () => { + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }) + + expect(driver.searchUserDirectory).not.toBeCalled() + }) + + it("should reject requests without search_term", async () => { + const event: IWidgetApiRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: {}, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - missing search term" }, + }) + + expect(driver.searchUserDirectory).not.toBeCalled() + }) + + it("should reject requests with a negative limit", async () => { + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: -1, + }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Invalid request - limit out of range" }, + }) + + expect(driver.searchUserDirectory).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.searchUserDirectory.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ) + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: + "Unexpected error while searching in the user directory", + }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.searchUserDirectory.mockRejectedValue( + new CustomMatrixError( + "failed to search the user directory", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ) + + const event: IUserDirectorySearchFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { search_term: "foo" }, + } + + await loadIframe(["org.matrix.msc3973.user_directory_search"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: + "Unexpected error while searching in the user directory", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to search the user directory", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("org.matrix.msc4039.get_media_config action", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }) + }) + + it("should handle and process the request", async () => { + driver.getMediaConfig.mockResolvedValue({ + "m.upload.size": 1000, + }) + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + "m.upload.size": 1000, + }) + }) + + expect(driver.getMediaConfig).toBeCalled() + }) + + it("should reject requests when the capability was not requested", async () => { + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }) + + expect(driver.getMediaConfig).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.getMediaConfig.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ) + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: + "Unexpected error while getting the media configuration", + }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.getMediaConfig.mockRejectedValue( + new CustomMatrixError( + "failed to get the media configuration", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ) + + const event: IGetMediaConfigActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: + "Unexpected error while getting the media configuration", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to get the media configuration", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("MSC4039", () => { + it("should present as supported api version", () => { + const event: ISupportedVersionsActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.SupportedApiVersions, + data: {}, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + supported_versions: expect.arrayContaining([ + UnstableApiVersion.MSC4039, + ]), + }) + }) + }) + + describe("org.matrix.msc4039.upload_file action", () => { + it("should handle and process the request", async () => { + driver.uploadFile.mockResolvedValue({ + contentUri: "mxc://...", + }) + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + content_uri: "mxc://...", + }) + }) + + expect(driver.uploadFile).toBeCalled() + }) + + it("should reject requests when the capability was not requested", async () => { + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }) + + expect(driver.uploadFile).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.uploadFile.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ) + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while uploading a file", + }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.uploadFile.mockRejectedValue( + new CustomMatrixError( + "failed to upload a file", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ) + + const event: IUploadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { + file: "data", + }, + } + + await loadIframe(["org.matrix.msc4039.upload_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while uploading a file", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to upload a file", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + describe("org.matrix.msc4039.download_file action", () => { + it("should handle and process the request", async () => { + driver.downloadFile.mockResolvedValue({ + file: "test contents", + }) + + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + } + + await loadIframe(["org.matrix.msc4039.download_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toHaveBeenCalledWith(event, { + file: "test contents", + }) + }) + + expect(driver.downloadFile).toHaveBeenCalledWith( + "mxc://example.com/test_file", + ) + }) + + it("should reject requests when the capability was not requested", async () => { + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + } + + emitEvent(new CustomEvent("", { detail: event })) + + expect(transport.reply).toBeCalledWith(event, { + error: { message: "Missing capability" }, + }) + + expect(driver.uploadFile).not.toBeCalled() + }) + + it("should reject requests when the driver throws an exception", async () => { + driver.downloadFile.mockRejectedValue( + new Error("M_LIMIT_EXCEEDED: Too many requests"), + ) + + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + } + + await loadIframe(["org.matrix.msc4039.download_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while downloading a file", + }, + }) + }) + }) + + it("should reject with Matrix API error response thrown by driver", async () => { + driver.processError.mockImplementation(processCustomMatrixError) + + driver.downloadFile.mockRejectedValue( + new CustomMatrixError( + "failed to download a file", + 429, + "M_LIMIT_EXCEEDED", + { + reason: "Too many requests", + retry_after_ms: 2000, + }, + ), + ) + + const event: IDownloadFileActionFromWidgetActionRequest = { + api: WidgetApiDirection.FromWidget, + widgetId: "test", + requestId: "0", + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { + content_uri: "mxc://example.com/test_file", + }, + } + + await loadIframe(["org.matrix.msc4039.download_file"]) + + emitEvent(new CustomEvent("", { detail: event })) + + await waitFor(() => { + expect(transport.reply).toBeCalledWith(event, { + error: { + message: "Unexpected error while downloading a file", + matrix_api_error: { + http_status: 429, + http_headers: {}, + url: "", + response: { + errcode: "M_LIMIT_EXCEEDED", + error: "failed to download a file", + reason: "Too many requests", + retry_after_ms: 2000, + }, + } satisfies IMatrixApiError, + }, + }) + }) + }) + }) + + it("updates theme", () => { + clientWidgetApi.updateTheme({ name: "dark" }) expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: [newTopicEvent], - }, - ); - }); - - // Up to this point we should not have received any state for the - // other (unviewed) room - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([otherRoomNameEvent]), - }, - ); - // Now view the other room - clientWidgetApi.setViewedRoomId(otherRoomId); - (transport.send as unknown as jest.SpyInstance).mockClear(); - - await waitFor(() => { - // The state of the other room should now be pushed + WidgetApiToWidgetAction.ThemeChange, + { name: "dark" }, + ) + }) + + it("updates language", () => { + clientWidgetApi.updateLanguage("tlh") expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([otherRoomNameEvent]), - }, - ); - }); - }); - }); - - describe("update_delayed_event action", () => { - it("fails to update delayed events", async () => { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.updateDelayedEvent).not.toBeCalled(); - }); - - it("fails to update delayed events with unsupported action", async () => { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: "unknown" as UpdateDelayedEventAction, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.updateDelayedEvent).not.toBeCalled(); - }); - - it("updates delayed events", async () => { - driver.updateDelayedEvent.mockResolvedValue(undefined); - - for (const action of [ - UpdateDelayedEventAction.Cancel, - UpdateDelayedEventAction.Restart, - UpdateDelayedEventAction.Send, - ]) { - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); - - expect(driver.updateDelayedEvent).toHaveBeenCalledWith( - event.data.delay_id, - event.data.action, - ); - } - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.updateDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ); - - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error updating delayed event" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.updateDelayedEvent.mockRejectedValue( - new CustomMatrixError( - "failed to update delayed event", - 400, - "M_NOT_JSON", - { - reason: "Content must be a JSON object.", - }, - ), - ); - - const event: IUpdateDelayedEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, - data: { - delay_id: "f", - action: UpdateDelayedEventAction.Send, - }, - }; - - await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error updating delayed event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_NOT_JSON", - error: "failed to update delayed event", - reason: "Content must be a JSON object.", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("send_to_device action", () => { - it("sends unencrypted to-device events", async () => { - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}); - }); - - expect(driver.sendToDevice).toHaveBeenCalledWith( - event.data.type, - event.data.encrypted, - event.data.messages, - ); - }); - - it("fails to send to-device events without event type", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event type" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); - - it("fails to send to-device events without event contents", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event contents" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); - - it("fails to send to-device events without encryption flag", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing encryption flag" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); - - it("fails to send to-device events with any event type", async () => { - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}_different`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Cannot send to-device events of this type" }, - }); - }); - - expect(driver.sendToDevice).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.sendToDevice.mockRejectedValue( - new Error( - "M_FORBIDDEN: You don't have permission to send to-device events", - ), - ); - - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Error sending event" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.sendToDevice.mockRejectedValue( - new CustomMatrixError("failed to send event", 400, "M_FORBIDDEN", { - reason: "You don't have permission to send to-device events", - }), - ); - - const event: ISendToDeviceFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SendToDevice, - data: { - type: "net.example.test", - encrypted: false, - messages: { - "@foo:bar.com": { - DEVICEID: { - example_content_key: "value", - }, - }, - }, - }, - }; - - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Error sending event", - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_FORBIDDEN", - error: "failed to send event", - reason: "You don't have permission to send to-device events", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("get_openid action", () => { - it("gets info", async () => { - driver.askOpenID.mockImplementation((observable) => { - observable.update({ - state: OpenIDRequestState.Allowed, - token: { - access_token: "access_token", - }, - }); - }); - - const event: IGetOpenIDActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.GetOpenIDCredentials, - data: {}, - }; - - await loadIframe([]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - state: OpenIDRequestState.Allowed, - access_token: "access_token", - }); - }); - - expect(driver.askOpenID).toHaveBeenCalledWith( - expect.any(SimpleObservable), - ); - }); - - it("fails when client provided invalid token", async () => { - driver.askOpenID.mockImplementation((observable) => { - observable.update({ - state: OpenIDRequestState.Allowed, - }); - }); - - const event: IGetOpenIDActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.GetOpenIDCredentials, - data: {}, - }; - - await loadIframe([]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - error: { - message: - "client provided invalid OIDC token for an allowed request", - }, - }); - }); - - expect(driver.askOpenID).toHaveBeenCalledWith( - expect.any(SimpleObservable), - ); - }); - }); - - describe("com.beeper.read_room_account_data action", () => { - it("reads room account data", async () => { - const type = "net.example.test"; - const roomId = "!room:example.org"; - - driver.readRoomAccountData.mockResolvedValue([ - { - type, - room_id: roomId, - content: {}, - }, - ]); - - const event: IReadRoomAccountDataFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, - data: { - room_ids: [roomId], - type, - }, - }; - - await loadIframe([ - `com.beeper.capabilities.receive.room_account_data:${type}`, - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - events: [ - { - type, - room_id: roomId, - content: {}, - }, - ], - }); - }); - - expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); - }); - - it("does not read room account data", async () => { - const type = "net.example.test"; - const roomId = "!room:example.org"; - - driver.readRoomAccountData.mockResolvedValue([ - { - type, - room_id: roomId, - content: {}, - }, - ]); - - const event: IReadRoomAccountDataFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData, - data: { - room_ids: [roomId], - type, - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - error: { message: "Cannot read room account data of this type" }, - }); - }); - - expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); - }); - }); - - describe("org.matrix.msc2876.read_events action", () => { - it("reads events from a specific room", async () => { - const roomId = "!room:example.org"; - const event = createRoomEvent({ - room_id: roomId, - type: "net.example.test", - content: "test", - }); - driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event]; - return []; - }); - - const request: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - room_ids: [roomId], - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${roomId}`, - "org.matrix.msc2762.receive.event:net.example.test", - ]); - clientWidgetApi.setViewedRoomId(roomId); - - emitEvent(new CustomEvent("", { detail: request })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(request, { - events: [event], - }); - }); - - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); - - it("reads events from all rooms", async () => { - const roomId = "!room:example.org"; - const otherRoomId = "!other-room:example.org"; - const event = createRoomEvent({ - room_id: roomId, - type: "net.example.test", - content: "test", - }); - const otherRoomEvent = createRoomEvent({ - room_id: otherRoomId, - type: "net.example.test", - content: "hi", - }); - driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]); - driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event]; - if (rId === otherRoomId) return [otherRoomEvent]; - return []; - }); - - const request: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - room_ids: Symbols.AnyRoom, - }, - }; - - await loadIframe([ - `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, - "org.matrix.msc2762.receive.event:net.example.test", - ]); - clientWidgetApi.setViewedRoomId(roomId); - - emitEvent(new CustomEvent("", { detail: request })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(request, { - events: [event, otherRoomEvent], - }); - }); - - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - roomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - expect(driver.readRoomTimeline).toHaveBeenCalledWith( - otherRoomId, - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); - - it("reads state events with any state key", async () => { - driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ]); - - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: true, - }, - }; - - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test", - ]); - clientWidgetApi.setViewedRoomId("!room-id"); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - events: [ - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ], - }); - }); - - expect(driver.readRoomTimeline).toBeCalledWith( - "!room-id", - "net.example.test", - undefined, - undefined, - 0, - undefined, - ); - }); - - it("fails to read state events with any state key", async () => { - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: true, - }, - }; - - await loadIframe([]); // Without the required capability - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.readRoomTimeline).not.toBeCalled(); - }); - - it("reads state events with a specific state key", async () => { - driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ]); - - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: "B", - }, - }; - - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test#B", - ]); - clientWidgetApi.setViewedRoomId("!room-id"); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - events: [ - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ], - }); - }); - - expect(driver.readRoomTimeline).toBeCalledWith( - "!room-id", - "net.example.test", - undefined, - "B", - 0, - undefined, - ); - }); - - it("fails to read state events with a specific state key", async () => { - const event: IReadEventFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC2876ReadEvents, - data: { - type: "net.example.test", - state_key: "B", - }, - }; - - // Request the capability for the wrong state key - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test#A", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: expect.any(String) }, - }); - }); - - expect(driver.readRoomTimeline).not.toBeCalled(); - }); - }); - - describe("org.matrix.msc3869.read_relations action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3869, - ]), - }); - }); - - it("should handle and process the request", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [createRoomEvent()], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe(["org.matrix.msc2762.receive.event:m.room.message"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [createRoomEvent()], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); - }); - - it("should only return events that match requested capabilities", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [ - createRoomEvent(), - createRoomEvent({ type: "m.reaction" }), - createRoomEvent({ type: "net.example.test", state_key: "A" }), - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe([ - "org.matrix.msc2762.receive.event:m.room.message", - "org.matrix.msc2762.receive.state_event:net.example.test#A", - ]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [ - createRoomEvent(), - createRoomEvent({ type: "net.example.test", state_key: "A" }), - ], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); - }); - - it("should accept all options and pass it to the driver", async () => { - driver.readEventRelations.mockResolvedValue({ - chunk: [], - }); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!room-id", - event_type: "m.room.message", - rel_type: "m.reference", - limit: 25, - from: "from-token", - to: "to-token", - direction: "f", - }, - }; - - await loadIframe(["org.matrix.msc2762.timeline:!room-id"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - chunk: [], - }); - }); - - expect(driver.readEventRelations).toBeCalledWith( - "$event", - "!room-id", - "m.reference", - "m.room.message", - "from-token", - "to-token", - 25, - "f", - ); - }); - - it("should reject requests without event_id", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing event ID" }, - }); - }); - - it("should reject requests with a negative limit", async () => { - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - limit: -1, - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - limit out of range" }, - }); - }); - - it("should reject requests when the room timeline was not requested", async () => { - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!another-room-id", - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unable to access room timeline: !another-room-id" }, - }); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.readEventRelations.mockRejectedValue( - new Error( - "M_FORBIDDEN: You don't have permission to access that event", - ), - ); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe(); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while reading relations" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.readEventRelations.mockRejectedValue( - new CustomMatrixError("failed to read relations", 403, "M_FORBIDDEN", { - reason: "You don't have permission to access that event", - }), - ); - - const event: IReadRelationsFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { event_id: "$event" }, - }; - - await loadIframe(); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while reading relations", - matrix_api_error: { - http_status: 403, - http_headers: {}, - url: "", - response: { - errcode: "M_FORBIDDEN", - error: "failed to read relations", - reason: "You don't have permission to access that event", - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("org.matrix.msc3973.user_directory_search action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3973, - ]), - }); - }); - - it("should handle and process the request", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: true, - results: [ - { - userId: "@foo:bar.com", - }, - ], - }); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: true, - results: [ - { - user_id: "@foo:bar.com", - display_name: undefined, - avatar_url: undefined, - }, - ], - }); - }); - - expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); - }); - - it("should accept all options and pass it to the driver", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: false, - results: [ - { - userId: "@foo:bar.com", - }, - { - userId: "@bar:foo.com", - displayName: "Bar", - avatarUrl: "mxc://...", - }, - ], - }); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: 5, - }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: false, - results: [ - { - user_id: "@foo:bar.com", - display_name: undefined, - avatar_url: undefined, - }, - { - user_id: "@bar:foo.com", - display_name: "Bar", - avatar_url: "mxc://...", - }, - ], - }); - }); - - expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); - }); - - it("should accept empty search_term", async () => { - driver.searchUserDirectory.mockResolvedValue({ - limited: false, - results: [], - }); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - limited: false, - results: [], - }); - }); - - expect(driver.searchUserDirectory).toBeCalledWith("", undefined); - }); - - it("should reject requests when the capability was not requested", async () => { - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - - expect(driver.searchUserDirectory).not.toBeCalled(); - }); - - it("should reject requests without search_term", async () => { - const event: IWidgetApiRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: {}, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - missing search term" }, - }); - - expect(driver.searchUserDirectory).not.toBeCalled(); - }); - - it("should reject requests with a negative limit", async () => { - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: -1, - }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Invalid request - limit out of range" }, - }); - - expect(driver.searchUserDirectory).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.searchUserDirectory.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while searching in the user directory", - }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.searchUserDirectory.mockRejectedValue( - new CustomMatrixError( - "failed to search the user directory", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ); - - const event: IUserDirectorySearchFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { search_term: "foo" }, - }; - - await loadIframe(["org.matrix.msc3973.user_directory_search"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while searching in the user directory", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to search the user directory", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("org.matrix.msc4039.get_media_config action", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), - }); - }); - - it("should handle and process the request", async () => { - driver.getMediaConfig.mockResolvedValue({ - "m.upload.size": 1000, - }); - - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - "m.upload.size": 1000, - }); - }); - - expect(driver.getMediaConfig).toBeCalled(); - }); - - it("should reject requests when the capability was not requested", async () => { - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - - expect(driver.getMediaConfig).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.getMediaConfig.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); - - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while getting the media configuration", - }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.getMediaConfig.mockRejectedValue( - new CustomMatrixError( - "failed to get the media configuration", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ); - - const event: IGetMediaConfigActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while getting the media configuration", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to get the media configuration", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("MSC4039", () => { - it("should present as supported api version", () => { - const event: ISupportedVersionsActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.SupportedApiVersions, - data: {}, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), - }); - }); - }); - - describe("org.matrix.msc4039.upload_file action", () => { - it("should handle and process the request", async () => { - driver.uploadFile.mockResolvedValue({ - contentUri: "mxc://...", - }); - - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - content_uri: "mxc://...", - }); - }); - - expect(driver.uploadFile).toBeCalled(); - }); - - it("should reject requests when the capability was not requested", async () => { - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - - expect(driver.uploadFile).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.uploadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); - - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while uploading a file" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.uploadFile.mockRejectedValue( - new CustomMatrixError( - "failed to upload a file", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ); - - const event: IUploadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { - file: "data", - }, - }; - - await loadIframe(["org.matrix.msc4039.upload_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while uploading a file", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to upload a file", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - describe("org.matrix.msc4039.download_file action", () => { - it("should handle and process the request", async () => { - driver.downloadFile.mockResolvedValue({ - file: "test contents", - }); - - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; - - await loadIframe(["org.matrix.msc4039.download_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, { - file: "test contents", - }); - }); - - expect(driver.downloadFile).toHaveBeenCalledWith( - "mxc://example.com/test_file", - ); - }); - - it("should reject requests when the capability was not requested", async () => { - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; - - emitEvent(new CustomEvent("", { detail: event })); - - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Missing capability" }, - }); - - expect(driver.uploadFile).not.toBeCalled(); - }); - - it("should reject requests when the driver throws an exception", async () => { - driver.downloadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ); - - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; - - await loadIframe(["org.matrix.msc4039.download_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { message: "Unexpected error while downloading a file" }, - }); - }); - }); - - it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError); - - driver.downloadFile.mockRejectedValue( - new CustomMatrixError( - "failed to download a file", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ); - - const event: IDownloadFileActionFromWidgetActionRequest = { - api: WidgetApiDirection.FromWidget, - widgetId: "test", - requestId: "0", - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { - content_uri: "mxc://example.com/test_file", - }, - }; - - await loadIframe(["org.matrix.msc4039.download_file"]); - - emitEvent(new CustomEvent("", { detail: event })); - - await waitFor(() => { - expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while downloading a file", - matrix_api_error: { - http_status: 429, - http_headers: {}, - url: "", - response: { - errcode: "M_LIMIT_EXCEEDED", - error: "failed to download a file", - reason: "Too many requests", - retry_after_ms: 2000, - }, - } satisfies IMatrixApiError, - }, - }); - }); - }); - }); - - it("updates theme", () => { - clientWidgetApi.updateTheme({ name: "dark" }); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.ThemeChange, - { name: "dark" }, - ); - }); - - it("updates language", () => { - clientWidgetApi.updateLanguage("tlh"); - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.LanguageChange, - { lang: "tlh" }, - ); - }); -}); + WidgetApiToWidgetAction.LanguageChange, + { lang: "tlh" }, + ) + }) +}) diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index c3870ab..da86955 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -15,842 +15,880 @@ * limitations under the License. */ -import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; -import { IGetMediaConfigActionFromWidgetResponseData } from "../src/interfaces/GetMediaConfigAction"; -import { IReadRelationsFromWidgetResponseData } from "../src/interfaces/ReadRelationsAction"; -import { ISendEventFromWidgetResponseData } from "../src/interfaces/SendEventAction"; -import { ISupportedVersionsActionResponseData } from "../src/interfaces/SupportedVersionsAction"; -import { IUploadFileActionFromWidgetResponseData } from "../src/interfaces/UploadFileAction"; -import { IDownloadFileActionFromWidgetResponseData } from "../src/interfaces/DownloadFileAction"; -import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/UserDirectorySearchAction"; -import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction"; -import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi"; +import { UnstableApiVersion } from "../src/interfaces/ApiVersion" +import { IGetMediaConfigActionFromWidgetResponseData } from "../src/interfaces/GetMediaConfigAction" +import { IReadRelationsFromWidgetResponseData } from "../src/interfaces/ReadRelationsAction" +import { ISendEventFromWidgetResponseData } from "../src/interfaces/SendEventAction" +import { ISupportedVersionsActionResponseData } from "../src/interfaces/SupportedVersionsAction" +import { IUploadFileActionFromWidgetResponseData } from "../src/interfaces/UploadFileAction" +import { IDownloadFileActionFromWidgetResponseData } from "../src/interfaces/DownloadFileAction" +import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/UserDirectorySearchAction" +import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction" +import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi" import { - IWidgetApiErrorResponseData, - IWidgetApiErrorResponseDataDetails, - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiResponse, - IWidgetApiResponseData, - UpdateDelayedEventAction, - WidgetApiDirection, -} from "../src"; + IWidgetApiErrorResponseData, + IWidgetApiErrorResponseDataDetails, + IWidgetApiRequest, + IWidgetApiRequestData, + IWidgetApiResponse, + IWidgetApiResponseData, + UpdateDelayedEventAction, + WidgetApiDirection, +} from "../src" type SendRequestArgs = { - action: WidgetApiFromWidgetAction; - data: IWidgetApiRequestData; -}; + action: WidgetApiFromWidgetAction + data: IWidgetApiRequestData +} class TransportChannels { - /** Data sent by widget requests */ - public readonly requestQueue: Array = []; - /** Responses to send as if from a client. Initialized with the response to {@link WidgetApi.start}*/ - public readonly responseQueue: IWidgetApiResponseData[] = [ - { supported_versions: [] } satisfies ISupportedVersionsActionResponseData, - ]; + /** Data sent by widget requests */ + public readonly requestQueue: Array = [] + /** Responses to send as if from a client. Initialized with the response to {@link WidgetApi.start}*/ + public readonly responseQueue: IWidgetApiResponseData[] = [ + { + supported_versions: [], + } satisfies ISupportedVersionsActionResponseData, + ] } class WidgetTransportHelper { - /** For ignoring the request sent by {@link WidgetApi.start} */ - private skippedFirstRequest = false; + /** For ignoring the request sent by {@link WidgetApi.start} */ + private skippedFirstRequest = false - public constructor(private channels: TransportChannels) {} + public constructor(private channels: TransportChannels) {} - public nextTrackedRequest(): SendRequestArgs | undefined { - if (!this.skippedFirstRequest) { - this.skippedFirstRequest = true; - this.channels.requestQueue.shift(); + public nextTrackedRequest(): SendRequestArgs | undefined { + if (!this.skippedFirstRequest) { + this.skippedFirstRequest = true + this.channels.requestQueue.shift() + } + return this.channels.requestQueue.shift() } - return this.channels.requestQueue.shift(); - } - public queueResponse(data: IWidgetApiResponseData): void { - this.channels.responseQueue.push(data); - } + public queueResponse(data: IWidgetApiResponseData): void { + this.channels.responseQueue.push(data) + } } class ClientTransportHelper { - public constructor(private channels: TransportChannels) {} - - public trackRequest( - action: WidgetApiFromWidgetAction, - data: IWidgetApiRequestData, - ): void { - this.channels.requestQueue.push({ action, data }); - } - - public nextQueuedResponse(): IWidgetApiRequestData | undefined { - return this.channels.responseQueue.shift(); - } + public constructor(private channels: TransportChannels) {} + + public trackRequest( + action: WidgetApiFromWidgetAction, + data: IWidgetApiRequestData, + ): void { + this.channels.requestQueue.push({ action, data }) + } + + public nextQueuedResponse(): IWidgetApiRequestData | undefined { + return this.channels.responseQueue.shift() + } } describe("WidgetApi", () => { - let widgetApi: WidgetApi; - let widgetTransportHelper: WidgetTransportHelper; - let clientListener: (e: MessageEvent) => void; - - beforeEach(() => { - const channels = new TransportChannels(); - widgetTransportHelper = new WidgetTransportHelper(channels); - const clientTrafficHelper = new ClientTransportHelper(channels); - - clientListener = (e: MessageEvent): void => { - if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response - if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) - return; // not a request - const request = e.data; - - clientTrafficHelper.trackRequest( - request.action as WidgetApiFromWidgetAction, - request.data, - ); - - const response = clientTrafficHelper.nextQueuedResponse(); - if (response) { - window.postMessage( - { - ...request, - response: response, - } satisfies IWidgetApiResponse, - "*", - ); - } - }; - window.addEventListener("message", clientListener); - - widgetApi = new WidgetApi("WidgetApi-test", "*"); - widgetApi.start(); - }); - - afterEach(() => { - window.removeEventListener("message", clientListener); - }); - - describe("readEventRelations", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - chunk: [], - } as IReadRelationsFromWidgetResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).resolves.toEqual({ - chunk: [], - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: { - event_id: "$event", - room_id: "!room-id", - rel_type: "m.reference", - event_type: "m.room.message", - limit: 25, - from: "from-token", - to: "to-token", - direction: "f", - }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [], - } as ISupportedVersionsActionResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow( - "The read_relations action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC3869ReadRelations, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.readEventRelations( - "$event", - "!room-id", - "m.reference", - "m.room.message", - 25, - "from-token", - "to-token", - "f", - ), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("sendEvent", () => { - it("sends message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - event_id: "$event", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).resolves.toEqual({ - room_id: "!room-id", - event_id: "$event", - }); - }); - - it("sends state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - event_id: "$event", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id"), - ).resolves.toEqual({ - room_id: "!room-id", - event_id: "$event", - }); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("delayed sendEvent", () => { - it("sends delayed message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000), - ).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 2000), - ).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed child action message events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("sends delayed child action state events", async () => { - widgetTransportHelper.queueResponse({ - room_id: "!room-id", - delay_id: "id", - } as ISendEventFromWidgetResponseData); - - await expect( - widgetApi.sendStateEvent( - "m.room.topic", - "", - {}, - "!room-id", - 1000, - undefined, - ), - ).resolves.toEqual({ - room_id: "!room-id", - delay_id: "id", - }); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("updateDelayedEvent", () => { - it("updates delayed events", async () => { - widgetTransportHelper.queueResponse({}); - await expect( - widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), - ).resolves.toEqual({}); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("getClientVersions", () => { + let widgetApi: WidgetApi + let widgetTransportHelper: WidgetTransportHelper + let clientListener: (e: MessageEvent) => void + beforeEach(() => { - widgetTransportHelper.queueResponse({ - supported_versions: [ - UnstableApiVersion.MSC3869, - UnstableApiVersion.MSC2762, - ], - } as ISupportedVersionsActionResponseData); - }); - - it("should request supported client versions", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]); - }); - - it("should cache supported client versions on successive calls", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]); - - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); - }); - }); - - describe("searchUserDirectory", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - limited: false, - results: [], - } as IUserDirectorySearchFromWidgetResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).resolves.toEqual({ - limited: false, - results: [], - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: { - search_term: "foo", - limit: 10, - }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [], - } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( - "The user_directory_search action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - }); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("getMediaConfig", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - "m.upload.size": 1000, - } as IGetMediaConfigActionFromWidgetResponseData); - - await expect(widgetApi.getMediaConfig()).resolves.toEqual({ - "m.upload.size": 1000, - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: {}, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [], - } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - "The get_media_config action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - }); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("uploadFile", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - content_uri: "mxc://...", - } as IUploadFileActionFromWidgetResponseData); - - await expect(widgetApi.uploadFile("data")).resolves.toEqual({ - content_uri: "mxc://...", - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, - data: { file: "data" }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [], - } as ISupportedVersionsActionResponseData); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - "The upload_file action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - }); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - "An error occurred", - ); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); - - describe("downloadFile", () => { - it("should forward the request to the ClientWidgetApi", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - file: "test contents", - } as IDownloadFileActionFromWidgetResponseData); - - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).resolves.toEqual({ - file: "test contents", - }); - - expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); - expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, - data: { content_uri: "mxc://example.com/test_file" }, - } satisfies SendRequestArgs); - }); - - it("should reject the request if the api is not supported", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [], - } as ISupportedVersionsActionResponseData); - - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow( - "The download_file action is not supported by the client.", - ); - - const request = widgetTransportHelper.nextTrackedRequest(); - expect(request).not.toBeUndefined(); - expect(request).not.toEqual({ - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, - data: expect.anything(), - } satisfies SendRequestArgs); - }); - - it("should handle an error", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - widgetTransportHelper.queueResponse({ - error: { message: "An error occurred" }, - }); - - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow("An error occurred"); - }); - - it("should handle an error with details", async () => { - widgetTransportHelper.queueResponse({ - supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData); - - const errorDetails: IWidgetApiErrorResponseDataDetails = { - matrix_api_error: { - http_status: 400, - http_headers: {}, - url: "", - response: { - errcode: "M_UNKNOWN", - error: "Unknown error", - }, - }, - }; - - widgetTransportHelper.queueResponse({ - error: { - message: "An error occurred", - ...errorDetails, - }, - } as IWidgetApiErrorResponseData); - - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ); - }); - }); -}); + const channels = new TransportChannels() + widgetTransportHelper = new WidgetTransportHelper(channels) + const clientTrafficHelper = new ClientTransportHelper(channels) + + clientListener = (e: MessageEvent): void => { + if (!e.data.action || !e.data.requestId || !e.data.widgetId) return // invalid request/response + if ( + "response" in e.data || + e.data.api !== WidgetApiDirection.FromWidget + ) + return // not a request + const request = e.data + + clientTrafficHelper.trackRequest( + request.action as WidgetApiFromWidgetAction, + request.data, + ) + + const response = clientTrafficHelper.nextQueuedResponse() + if (response) { + window.postMessage( + { + ...request, + response: response, + } satisfies IWidgetApiResponse, + "*", + ) + } + } + window.addEventListener("message", clientListener) + + widgetApi = new WidgetApi("WidgetApi-test", "*") + widgetApi.start() + }) + + afterEach(() => { + window.removeEventListener("message", clientListener) + }) + + describe("readEventRelations", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + chunk: [], + } as IReadRelationsFromWidgetResponseData) + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).resolves.toEqual({ + chunk: [], + }) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: { + event_id: "$event", + room_id: "!room-id", + rel_type: "m.reference", + event_type: "m.room.message", + limit: 25, + from: "from-token", + to: "to-token", + direction: "f", + }, + } satisfies SendRequestArgs) + }) + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData) + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow( + "The read_relations action is not supported by the client.", + ) + + const request = widgetTransportHelper.nextTrackedRequest() + expect(request).not.toBeUndefined() + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC3869ReadRelations, + data: expect.anything(), + } satisfies SendRequestArgs) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3869], + } as ISupportedVersionsActionResponseData) + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.readEventRelations( + "$event", + "!room-id", + "m.reference", + "m.room.message", + 25, + "from-token", + "to-token", + "f", + ), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("sendEvent", () => { + it("sends message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + event_id: "$event", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", + }) + }) + + it("sends state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + event_id: "$event", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id"), + ).resolves.toEqual({ + room_id: "!room-id", + event_id: "$event", + }) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("delayed sendEvent", () => { + it("sends delayed message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }) + }) + + it("sends delayed state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendStateEvent( + "m.room.topic", + "", + {}, + "!room-id", + 2000, + ), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }) + }) + + it("sends delayed child action message events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }) + }) + + it("sends delayed child action state events", async () => { + widgetTransportHelper.queueResponse({ + room_id: "!room-id", + delay_id: "id", + } as ISendEventFromWidgetResponseData) + + await expect( + widgetApi.sendStateEvent( + "m.room.topic", + "", + {}, + "!room-id", + 1000, + undefined, + ), + ).resolves.toEqual({ + room_id: "!room-id", + delay_id: "id", + }) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.sendRoomEvent( + "m.room.message", + {}, + "!room-id", + 1000, + undefined, + ), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("updateDelayedEvent", () => { + it("updates delayed events", async () => { + widgetTransportHelper.queueResponse({}) + await expect( + widgetApi.updateDelayedEvent( + "id", + UpdateDelayedEventAction.Send, + ), + ).resolves.toEqual({}) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.updateDelayedEvent( + "id", + UpdateDelayedEventAction.Send, + ), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.updateDelayedEvent( + "id", + UpdateDelayedEventAction.Send, + ), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("getClientVersions", () => { + beforeEach(() => { + widgetTransportHelper.queueResponse({ + supported_versions: [ + UnstableApiVersion.MSC3869, + UnstableApiVersion.MSC2762, + ], + } as ISupportedVersionsActionResponseData) + }) + + it("should request supported client versions", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]) + }) + + it("should cache supported client versions on successive calls", async () => { + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]) + + await expect(widgetApi.getClientVersions()).resolves.toEqual([ + "org.matrix.msc3869", + "org.matrix.msc2762", + ]) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined() + }) + }) + + describe("searchUserDirectory", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + limited: false, + results: [], + } as IUserDirectorySearchFromWidgetResponseData) + + await expect( + widgetApi.searchUserDirectory("foo", 10), + ).resolves.toEqual({ + limited: false, + results: [], + }) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: { + search_term: "foo", + limit: 10, + }, + } satisfies SendRequestArgs) + }) + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData) + + await expect( + widgetApi.searchUserDirectory("foo", 10), + ).rejects.toThrow( + "The user_directory_search action is not supported by the client.", + ) + + const request = widgetTransportHelper.nextTrackedRequest() + expect(request).not.toBeUndefined() + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, + data: expect.anything(), + } satisfies SendRequestArgs) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }) + + await expect( + widgetApi.searchUserDirectory("foo", 10), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC3973], + } as ISupportedVersionsActionResponseData) + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.searchUserDirectory("foo", 10), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("getMediaConfig", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + "m.upload.size": 1000, + } as IGetMediaConfigActionFromWidgetResponseData) + + await expect(widgetApi.getMediaConfig()).resolves.toEqual({ + "m.upload.size": 1000, + }) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: {}, + } satisfies SendRequestArgs) + }) + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData) + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + "The get_media_config action is not supported by the client.", + ) + + const request = widgetTransportHelper.nextTrackedRequest() + expect(request).not.toBeUndefined() + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }) + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + "An error occurred", + ) + }) + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect(widgetApi.getMediaConfig()).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("uploadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + content_uri: "mxc://...", + } as IUploadFileActionFromWidgetResponseData) + + await expect(widgetApi.uploadFile("data")).resolves.toEqual({ + content_uri: "mxc://...", + }) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data: { file: "data" }, + } satisfies SendRequestArgs) + }) + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData) + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "The upload_file action is not supported by the client.", + ) + + const request = widgetTransportHelper.nextTrackedRequest() + expect(request).not.toBeUndefined() + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }) + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + "An error occurred", + ) + }) + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect(widgetApi.uploadFile("data")).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) + + describe("downloadFile", () => { + it("should forward the request to the ClientWidgetApi", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + file: "test contents", + } as IDownloadFileActionFromWidgetResponseData) + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).resolves.toEqual({ + file: "test contents", + }) + + expect( + widgetTransportHelper.nextTrackedRequest(), + ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data: { content_uri: "mxc://example.com/test_file" }, + } satisfies SendRequestArgs) + }) + + it("should reject the request if the api is not supported", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [], + } as ISupportedVersionsActionResponseData) + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow( + "The download_file action is not supported by the client.", + ) + + const request = widgetTransportHelper.nextTrackedRequest() + expect(request).not.toBeUndefined() + expect(request).not.toEqual({ + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, + data: expect.anything(), + } satisfies SendRequestArgs) + }) + + it("should handle an error", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + widgetTransportHelper.queueResponse({ + error: { message: "An error occurred" }, + }) + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow("An error occurred") + }) + + it("should handle an error with details", async () => { + widgetTransportHelper.queueResponse({ + supported_versions: [UnstableApiVersion.MSC4039], + } as ISupportedVersionsActionResponseData) + + const errorDetails: IWidgetApiErrorResponseDataDetails = { + matrix_api_error: { + http_status: 400, + http_headers: {}, + url: "", + response: { + errcode: "M_UNKNOWN", + error: "Unknown error", + }, + }, + } + + widgetTransportHelper.queueResponse({ + error: { + message: "An error occurred", + ...errorDetails, + }, + } as IWidgetApiErrorResponseData) + + await expect( + widgetApi.downloadFile("mxc://example.com/test_file"), + ).rejects.toThrow( + new WidgetApiResponseError("An error occurred", errorDetails), + ) + }) + }) +}) diff --git a/test/url-template-test.ts b/test/url-template-test.ts index ee67028..549ad1d 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -14,50 +14,50 @@ * limitations under the License. */ -import { runTemplate } from "../src"; +import { runTemplate } from "../src" describe("runTemplate", () => { - it("should replace device id template in url", () => { - const url = - "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; - const replacedUrl = runTemplate( - url, - { - id: "widget-id", - creatorUserId: "@user-id", - type: "type", - url, - }, - { - deviceId: "my-device-id", - currentUserId: "@user-id", - }, - ); + it("should replace device id template in url", () => { + const url = + "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id" + const replacedUrl = runTemplate( + url, + { + id: "widget-id", + creatorUserId: "@user-id", + type: "type", + url, + }, + { + deviceId: "my-device-id", + currentUserId: "@user-id", + }, + ) - expect(replacedUrl).toBe( - "https://localhost/?my-query#device_id=my-device-id", - ); - }); + expect(replacedUrl).toBe( + "https://localhost/?my-query#device_id=my-device-id", + ) + }) - it("should replace base url template in url", () => { - const url = - "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; - const replacedUrl = runTemplate( - url, - { - id: "widget-id", - creatorUserId: "@user-id", - type: "type", - url, - }, - { - currentUserId: "@user-id", - baseUrl: "https://localhost/api", - }, - ); + it("should replace base url template in url", () => { + const url = + "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url" + const replacedUrl = runTemplate( + url, + { + id: "widget-id", + creatorUserId: "@user-id", + type: "type", + url, + }, + { + currentUserId: "@user-id", + baseUrl: "https://localhost/api", + }, + ) - expect(replacedUrl).toBe( - "https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi", - ); - }); -}); + expect(replacedUrl).toBe( + "https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi", + ) + }) +}) diff --git a/tsconfig-dev.json b/tsconfig-dev.json index 5ef424e..1415bcb 100644 --- a/tsconfig-dev.json +++ b/tsconfig-dev.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.json", - "include": ["./test/**/*.ts"] + "extends": "./tsconfig.json", + "include": ["./test/**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index f58ceb1..e5261eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { - "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "esModuleInterop": true, - "module": "commonjs", - "moduleResolution": "node", - "target": "es2016", - "sourceMap": true, - "outDir": "./lib", - "declaration": true, - "types": ["jest"], - "lib": ["es2020", "dom"], - "strict": true - }, - "include": ["./src/**/*.ts"] + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es2016", + "sourceMap": true, + "outDir": "./lib", + "declaration": true, + "types": ["jest"], + "lib": ["es2020", "dom"], + "strict": true + }, + "include": ["./src/**/*.ts"] } From e7e24881fef0e33ee85d4336c027df3a447a4bfc Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 18:46:14 +0100 Subject: [PATCH 10/11] review --- .eslintrc.js | 2 +- .github/workflows/build.yaml | 3 +++ .prettierrc | 4 ---- .prettierrc.js | 1 + .vscode/settings.json | 3 +++ 5 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 .prettierrc create mode 100644 .prettierrc.js create mode 100644 .vscode/settings.json diff --git a/.eslintrc.js b/.eslintrc.js index 57b1a39..46720e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,7 @@ module.exports = { overrides: [ { files: ["src/**/*.ts", "test/**/*.ts"], - extends: ["plugin:matrix-org/typescript", "prettier"], + extends: ["plugin:matrix-org/typescript"], rules: { // TypeScript has its own version of this "babel/no-invalid-this": "off", diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a7c35cd..3ace968 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,6 +22,9 @@ jobs: - name: Check Linting Rules and Types run: yarn lint + + - name: Check Formatting + run: yarn prettier:check - name: test run: yarn test --coverage diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 257a7df..0000000 --- a/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tabWidth": 4, - "semi": false -} diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..705e89b --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("eslint-plugin-matrix-org/.prettierrc.js"); \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7c2feb7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": false +} From 7b9643e8180a77d3570e8e4efa39926a9a729272 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 18:46:57 +0100 Subject: [PATCH 11/11] apply prettier --- .eslintrc.js | 10 +- .github/workflows/build.yaml | 2 +- .prettierrc.js | 2 +- .vscode/settings.json | 2 +- README.md | 56 +- examples/widget/index.html | 46 +- examples/widget/utils.js | 18 +- jest.config.ts | 10 +- src/ClientWidgetApi.ts | 1226 +++++-------- src/WidgetApi.ts | 723 +++----- src/driver/WidgetDriver.ts | 120 +- src/index.ts | 108 +- src/interfaces/ApiVersion.ts | 4 +- src/interfaces/Capabilities.ts | 27 +- src/interfaces/CapabilitiesAction.ts | 63 +- src/interfaces/ContentLoadedAction.ts | 18 +- src/interfaces/DownloadFileAction.ts | 28 +- src/interfaces/GetMediaConfigAction.ts | 26 +- src/interfaces/GetOpenIDAction.ts | 26 +- src/interfaces/ICustomWidgetData.ts | 4 +- src/interfaces/IJitsiWidgetData.ts | 8 +- src/interfaces/IRoomAccountData.ts | 6 +- src/interfaces/IRoomEvent.ts | 16 +- src/interfaces/IStickerpickerWidgetData.ts | 2 +- src/interfaces/IWidget.ts | 20 +- src/interfaces/IWidgetApiErrorResponse.ts | 38 +- src/interfaces/IWidgetApiRequest.ts | 18 +- src/interfaces/IWidgetApiResponse.ts | 9 +- src/interfaces/LanguageChangeAction.ts | 20 +- src/interfaces/ModalWidgetActions.ts | 71 +- src/interfaces/NavigateAction.ts | 14 +- src/interfaces/OpenIDCredentialsAction.ts | 28 +- src/interfaces/ReadEventAction.ts | 36 +- src/interfaces/ReadRelationsAction.ts | 48 +- src/interfaces/ReadRoomAccountDataAction.ts | 34 +- src/interfaces/ScreenshotAction.ts | 17 +- src/interfaces/SendEventAction.ts | 54 +- src/interfaces/SendToDeviceAction.ts | 53 +- src/interfaces/SetModalButtonEnabledAction.ts | 24 +- src/interfaces/StickerAction.ts | 40 +- src/interfaces/StickyAction.ts | 16 +- src/interfaces/SupportedVersionsAction.ts | 30 +- src/interfaces/ThemeChangeAction.ts | 12 +- src/interfaces/TurnServerActions.ts | 44 +- src/interfaces/UpdateDelayedEventAction.ts | 28 +- src/interfaces/UpdateStateAction.ts | 22 +- src/interfaces/UploadFileAction.ts | 28 +- src/interfaces/UserDirectorySearchAction.ts | 38 +- src/interfaces/VisibilityAction.ts | 14 +- src/interfaces/WidgetApiAction.ts | 5 +- src/interfaces/WidgetApiDirection.ts | 6 +- src/interfaces/WidgetConfigAction.ts | 17 +- src/interfaces/WidgetType.ts | 2 +- src/models/Widget.ts | 40 +- src/models/WidgetEventCapability.ts | 232 +-- src/models/WidgetParser.ts | 99 +- src/models/validation/url.ts | 12 +- src/models/validation/utils.ts | 7 +- src/templating/url-template.ts | 55 +- src/transport/ITransport.ts | 32 +- src/transport/PostmessageTransport.ts | 172 +- src/util/SimpleObservable.ts | 12 +- test/ClientWidgetApi-test.ts | 1604 ++++++++--------- test/WidgetApi-test.ts | 586 +++--- test/url-template-test.ts | 26 +- 65 files changed, 2495 insertions(+), 3719 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 46720e3..96ca83a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,7 +14,7 @@ module.exports = { "one-var": ["warn"], "padded-blocks": ["warn"], "no-extend-native": ["warn"], - camelcase: ["warn"], + "camelcase": ["warn"], "no-multi-spaces": ["error", { ignoreEOLComments: true }], "space-before-function-paren": [ "error", @@ -26,8 +26,8 @@ module.exports = { ], "arrow-parens": "off", "prefer-promise-reject-errors": "off", - quotes: "off", - indent: "off", + "quotes": "off", + "indent": "off", "no-constant-condition": "off", "no-async-promise-executor": "off", }, @@ -39,7 +39,7 @@ module.exports = { // TypeScript has its own version of this "babel/no-invalid-this": "off", - quotes: "off", + "quotes": "off", }, }, { @@ -49,4 +49,4 @@ module.exports = { }, }, ], -} +}; diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3ace968..4e25fe4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,7 +22,7 @@ jobs: - name: Check Linting Rules and Types run: yarn lint - + - name: Check Formatting run: yarn prettier:check diff --git a/.prettierrc.js b/.prettierrc.js index 705e89b..6a17910 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1 +1 @@ -module.exports = require("eslint-plugin-matrix-org/.prettierrc.js"); \ No newline at end of file +module.exports = require("eslint-plugin-matrix-org/.prettierrc.js"); diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c2feb7..d3def91 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.formatOnSave": false + "editor.formatOnSave": false } diff --git a/README.md b/README.md index f092d98..a29fac5 100644 --- a/README.md +++ b/README.md @@ -36,40 +36,34 @@ to instantiate the `WidgetApi` class. The general usage for this would be: ```typescript -const widgetId = null // if you know the widget ID, supply it. -const api = new WidgetApi(widgetId) +const widgetId = null; // if you know the widget ID, supply it. +const api = new WidgetApi(widgetId); // Before doing anything else, request capabilities: -api.requestCapability(MatrixCapabilities.Screenshots) -api.requestCapabilities(StickerpickerCapabilities) +api.requestCapability(MatrixCapabilities.Screenshots); +api.requestCapabilities(StickerpickerCapabilities); // Add custom action handlers (if needed) -api.on( - `action:${WidgetApiToWidgetAction.UpdateVisibility}`, - (ev: CustomEvent) => { - ev.preventDefault() // we're handling it, so stop the widget API from doing something. - console.log(ev.detail) // custom handling here - api.transport.reply(ev.detail, {}) - }, -) -api.on( - "action:com.example.my_action", - (ev: CustomEvent) => { - ev.preventDefault() // we're handling it, so stop the widget API from doing something. - console.log(ev.detail) // custom handling here - api.transport.reply(ev.detail, { custom: "reply" }) - }, -) +api.on(`action:${WidgetApiToWidgetAction.UpdateVisibility}`, (ev: CustomEvent) => { + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, {}); +}); +api.on("action:com.example.my_action", (ev: CustomEvent) => { + ev.preventDefault(); // we're handling it, so stop the widget API from doing something. + console.log(ev.detail); // custom handling here + api.transport.reply(ev.detail, { custom: "reply" }); +}); // Start the messaging -api.start() +api.start(); // If waitForIframeLoad is false, tell the client that we're good to go -api.sendContentLoaded() +api.sendContentLoaded(); // Later, do something else (if needed) -api.setAlwaysOnScreen(true) -api.transport.send("com.example.my_action", { isExample: true }) +api.setAlwaysOnScreen(true); +api.transport.send("com.example.my_action", { isExample: true }); ``` For a more complete example, see the `examples` directory of this repo. @@ -83,17 +77,15 @@ SDK to provide an interface for other platforms. TODO: Improve this ```typescript -const driver = new CustomDriver() // an implementation of WidgetDriver -const api = new ClientWidgetApi(widget, iframe, driver) +const driver = new CustomDriver(); // an implementation of WidgetDriver +const api = new ClientWidgetApi(widget, iframe, driver); // The API is automatically started, so we just have to wait for a ready before doing something api.on("ready", () => { - api.updateVisibility(true).then(() => - console.log("Widget knows it is visible now"), - ) - api.transport.send("com.example.my_action", { isExample: true }) -}) + api.updateVisibility(true).then(() => console.log("Widget knows it is visible now")); + api.transport.send("com.example.my_action", { isExample: true }); +}); // Eventually, stop the API handling -api.stop() +api.stop(); ``` diff --git a/examples/widget/index.html b/examples/widget/index.html index b1f8b73..92fa58f 100644 --- a/examples/widget/index.html +++ b/examples/widget/index.html @@ -56,63 +56,57 @@ diff --git a/examples/widget/utils.js b/examples/widget/utils.js index daccb83..705a6f0 100644 --- a/examples/widget/utils.js +++ b/examples/widget/utils.js @@ -15,21 +15,17 @@ */ function parseFragment() { - const fragmentString = window.location.hash || "?" - return new URLSearchParams( - fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0)), - ) + const fragmentString = window.location.hash || "?"; + return new URLSearchParams(fragmentString.substring(Math.max(fragmentString.indexOf("?"), 0))); } function assertParam(fragment, name) { - const val = fragment.get(name) - if (!val) - throw new Error(`${name} is not present in URL - cannot load widget`) - return val + const val = fragment.get(name); + if (!val) throw new Error(`${name} is not present in URL - cannot load widget`); + return val; } function handleError(e) { - console.error(e) - document.getElementById("container").innerText = - "There was an error with the widget. See JS console for details." + console.error(e); + document.getElementById("container").innerText = "There was an error with the widget. See JS console for details."; } diff --git a/jest.config.ts b/jest.config.ts index f9bd95b..6c6af37 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { env } from "process" +import { env } from "process"; -import type { Config } from "jest" +import type { Config } from "jest"; const config: Config = { testEnvironment: "jsdom", @@ -24,11 +24,11 @@ const config: Config = { collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], coverageReporters: ["text-summary", "lcov"], testResultsProcessor: "@casualbot/jest-sonar-reporter", -} +}; // if we're running under GHA, enable the GHA reporter if (env["GITHUB_ACTIONS"] !== undefined) { - config.reporters = [["github-actions", { silent: false }], "summary"] + config.reporters = [["github-actions", { silent: false }], "summary"]; } -export default config +export default config; diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index e6a9e82..bb148a6 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -14,123 +14,102 @@ * 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 { 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, getTimelineRoomIDFromCapability, isTimelineCapability, -} from "./interfaces/Capabilities" -import { - IOpenIDUpdate, - ISendEventDetails, - ISendDelayedEventDetails, - WidgetDriver, -} from "./driver/WidgetDriver" +} from "./interfaces/Capabilities"; +import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, WidgetDriver } from "./driver/WidgetDriver"; import { ICapabilitiesActionResponseData, INotifyCapabilitiesActionRequestData, IRenegotiateCapabilitiesActionRequest, -} from "./interfaces/CapabilitiesAction" +} 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" +} 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" +} from "./interfaces/ModalWidgetActions"; import { ISendEventFromWidgetActionRequest, ISendEventFromWidgetResponseData, ISendEventToWidgetRequestData, -} from "./interfaces/SendEventAction" +} from "./interfaces/SendEventAction"; import { ISendToDeviceFromWidgetActionRequest, ISendToDeviceFromWidgetResponseData, ISendToDeviceToWidgetRequestData, -} from "./interfaces/SendToDeviceAction" -import { - EventDirection, - EventKind, - WidgetEventCapability, -} from "./models/WidgetEventCapability" -import { IRoomEvent } from "./interfaces/IRoomEvent" -import { IRoomAccountData } from "./interfaces/IRoomAccountData" +} from "./interfaces/SendToDeviceAction"; +import { EventDirection, EventKind, WidgetEventCapability } from "./models/WidgetEventCapability"; +import { IRoomEvent } from "./interfaces/IRoomEvent"; +import { IRoomAccountData } from "./interfaces/IRoomAccountData"; 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" +} 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" +} from "./interfaces/TurnServerActions"; +import { Symbols } from "./Symbols"; import { IReadRelationsFromWidgetActionRequest, IReadRelationsFromWidgetResponseData, -} from "./interfaces/ReadRelationsAction" +} from "./interfaces/ReadRelationsAction"; import { IUserDirectorySearchFromWidgetActionRequest, IUserDirectorySearchFromWidgetResponseData, -} from "./interfaces/UserDirectorySearchAction" +} from "./interfaces/UserDirectorySearchAction"; import { IReadRoomAccountDataFromWidgetActionRequest, IReadRoomAccountDataFromWidgetResponseData, -} from "./interfaces/ReadRoomAccountDataAction" +} from "./interfaces/ReadRoomAccountDataAction"; import { IGetMediaConfigActionFromWidgetActionRequest, IGetMediaConfigActionFromWidgetResponseData, -} from "./interfaces/GetMediaConfigAction" +} from "./interfaces/GetMediaConfigAction"; import { IUpdateDelayedEventFromWidgetActionRequest, UpdateDelayedEventAction, -} from "./interfaces/UpdateDelayedEventAction" +} from "./interfaces/UpdateDelayedEventAction"; import { IUploadFileActionFromWidgetActionRequest, IUploadFileActionFromWidgetResponseData, -} from "./interfaces/UploadFileAction" +} from "./interfaces/UploadFileAction"; import { IDownloadFileActionFromWidgetActionRequest, IDownloadFileActionFromWidgetResponseData, -} from "./interfaces/DownloadFileAction" -import { IThemeChangeActionRequestData } from "./interfaces/ThemeChangeAction" -import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" +} from "./interfaces/DownloadFileAction"; +import { IThemeChangeActionRequestData } from "./interfaces/ThemeChangeAction"; +import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction"; /** * API handler for the client side of widgets. This raises events @@ -157,23 +136,20 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" * This class only handles one widget at a time. */ export class ClientWidgetApi extends EventEmitter { - public readonly transport: ITransport + 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 - private contentLoadedWaitTimer?: ReturnType + private contentLoadedActionSent = false; + private allowedCapabilities = new Set(); + private allowedEvents: WidgetEventCapability[] = []; + private isStopped = false; + private turnServers: AsyncGenerator | null = null; + private contentLoadedWaitTimer?: ReturnType; // Stores pending requests to push a room's state to the widget - private pushRoomStateTasks = new Set>() + private pushRoomStateTasks = new Set>(); // Room ID → event type → state key → events to be pushed - private pushRoomStateResult = new Map< - string, - Map> - >() - private flushRoomStateTask: Promise | null = null + private pushRoomStateResult = new Map>>(); + private flushRoomStateTask: Promise | null = null; /** * Creates a new client widget API. This will instantiate the transport @@ -188,172 +164,124 @@ export class ClientWidgetApi extends EventEmitter { private iframe: HTMLIFrameElement, private driver: WidgetDriver, ) { - super() + super(); if (!iframe?.contentWindow) { - throw new Error("No iframe supplied") + throw new Error("No iframe supplied"); } if (!widget) { - throw new Error("Invalid widget") + throw new Error("Invalid widget"); } if (!driver) { - throw new Error("Invalid 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() + 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) + 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}`) - ) + 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 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), - ) + 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), - ) + 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 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 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), - ) + return this.allowedEvents.some((e) => e.matchesAsToDeviceEvent(EventDirection.Receive, eventType)); } public canReceiveRoomAccountData(eventType: string): boolean { - return this.allowedEvents.some((e) => - e.matchesAsRoomAccountData(EventDirection.Receive, eventType), - ) + return this.allowedEvents.some((e) => e.matchesAsRoomAccountData(EventDirection.Receive, eventType)); } public stop(): void { - this.isStopped = true - this.transport.stop() + this.isStopped = true; + this.transport.stop(); } private beginCapabilities(): void { // widget has loaded - tell all the listeners that - this.emit("preparing") + this.emit("preparing"); - let requestedCaps: Capability[] + let requestedCaps: Capability[]; this.transport - .send( - WidgetApiToWidgetAction.Capabilities, - {}, - ) + .send(WidgetApiToWidgetAction.Capabilities, {}) .then((caps) => { - requestedCaps = caps.capabilities - return this.driver.validateCapabilities( - new Set(caps.capabilities), - ) + requestedCaps = caps.capabilities; + return this.driver.validateCapabilities(new Set(caps.capabilities)); }) .then((allowedCaps) => { - this.allowCapabilities([...allowedCaps], requestedCaps) - this.emit("ready") + this.allowCapabilities([...allowedCaps], requestedCaps); + this.emit("ready"); }) .catch((e) => { - this.emit("error:preparing", e) - }) + this.emit("error:preparing", e); + }); } private allowCapabilities(allowed: string[], requested: string[]): void { - console.log( - `Widget ${this.widget.id} is allowed capabilities:`, - allowed, - ) + console.log(`Widget ${this.widget.id} is allowed capabilities:`, allowed); - for (const c of allowed) this.allowedCapabilities.add(c) - const allowedEvents = - WidgetEventCapability.findEventCapabilities(allowed) - this.allowedEvents.push(...allowedEvents) + for (const c of allowed) this.allowedCapabilities.add(c); + const allowedEvents = WidgetEventCapability.findEventCapabilities(allowed); + this.allowedEvents.push(...allowedEvents); this.transport - .send(WidgetApiToWidgetAction.NotifyCapabilities, < - INotifyCapabilitiesActionRequestData - >{ + .send(WidgetApiToWidgetAction.NotifyCapabilities, { requested, approved: Array.from(this.allowedCapabilities), }) .catch((e) => { - console.warn( - "non-fatal error notifying widget of approved capabilities:", - e, - ) + console.warn("non-fatal error notifying widget of approved capabilities:", e); }) .then(() => { - this.emit("capabilitiesNotified") - }) + this.emit("capabilitiesNotified"); + }); // Push the initial room state for all rooms with a timeline capability for (const c of allowed) { if (isTimelineCapability(c)) { - const roomId = getTimelineRoomIDFromCapability(c) + const roomId = getTimelineRoomIDFromCapability(c); if (roomId === Symbols.AnyRoom) { - for (const roomId of this.driver.getKnownRooms()) - this.pushRoomState(roomId) + for (const roomId of this.driver.getKnownRooms()) this.pushRoomState(roomId); } else { - this.pushRoomState(roomId) + this.pushRoomState(roomId); } } } // If new events are allowed and the currently viewed room isn't covered // by a timeline capability, then we know that there could be some state // in the viewed room that the widget hasn't learned about yet- push it. - if ( - allowedEvents.length > 0 && - this.viewedRoomId !== null && - !this.canUseRoomTimeline(this.viewedRoomId) - ) { - this.pushRoomState(this.viewedRoomId) + if (allowedEvents.length > 0 && this.viewedRoomId !== null && !this.canUseRoomTimeline(this.viewedRoomId)) { + this.pushRoomState(this.viewedRoomId); } } @@ -361,34 +289,30 @@ export class ClientWidgetApi extends EventEmitter { 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() + this.beginCapabilities(); } else { // Reaching this means, that the Iframe got reloaded/loaded and // the clientApi is awaiting the FIRST ContentLoaded action. - console.log( - "waitForIframeLoad is false: waiting for widget to send contentLoaded", - ) + console.log("waitForIframeLoad is false: waiting for widget to send contentLoaded"); this.contentLoadedWaitTimer = setTimeout(() => { console.error( "Widget specified waitForIframeLoad=false but timed out waiting for contentLoaded event!", - ) - }, 10000) - this.contentLoadedActionSent = false + ); + }, 10000); + this.contentLoadedActionSent = false; } } - private handleContentLoadedAction( - action: IContentLoadedActionRequest, - ): void { + private handleContentLoadedAction(action: IContentLoadedActionRequest): void { if (this.contentLoadedWaitTimer !== undefined) { - clearTimeout(this.contentLoadedWaitTimer) - this.contentLoadedWaitTimer = undefined + clearTimeout(this.contentLoadedWaitTimer); + this.contentLoadedWaitTimer = undefined; } if (this.contentLoadedActionSent) { throw new Error( "Improper sequence: ContentLoaded Action can only be sent once after the widget loaded " + "and should only be used if waitForIframeLoad is false (default=true)", - ) + ); } if (this.widget.waitForIframeLoad) { this.transport.reply(action, { @@ -397,86 +321,74 @@ export class ClientWidgetApi extends EventEmitter { "Improper sequence: not expecting ContentLoaded event if " + "waitForIframeLoad is true (default=true)", }, - }) + }); } else { - this.transport.reply(action, {}) - this.beginCapabilities() + this.transport.reply(action, {}); + this.beginCapabilities(); } - this.contentLoadedActionSent = true + this.contentLoadedActionSent = true; } private replyVersions(request: ISupportedVersionsActionRequest): void { this.transport.reply(request, { supported_versions: CurrentApiVersions, - }) + }); } - private handleCapabilitiesRenegotiate( - request: IRenegotiateCapabilitiesActionRequest, - ): void { + private handleCapabilitiesRenegotiate(request: IRenegotiateCapabilitiesActionRequest): void { // acknowledge first - this.transport.reply(request, {}) + this.transport.reply(request, {}); - const requested = request.data?.capabilities || [] - const newlyRequested = new Set( - requested.filter((r) => !this.hasCapability(r)), - ) + const requested = request.data?.capabilities || []; + const newlyRequested = new Set(requested.filter((r) => !this.hasCapability(r))); if (newlyRequested.size === 0) { // Nothing to do - skip validation - this.allowCapabilities([], []) + this.allowCapabilities([], []); } this.driver .validateCapabilities(newlyRequested) - .then((allowed) => - this.allowCapabilities([...allowed], [...newlyRequested]), - ) + .then((allowed) => this.allowCapabilities([...allowed], [...newlyRequested])); } private handleNavigate(request: INavigateActionRequest): void { 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/#") - ) { + 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: unknown): void => { - console.error("[ClientWidgetApi] Failed to handle navigation: ", e) - this.handleDriverError(e, request, "Error handling navigation") - } + console.error("[ClientWidgetApi] Failed to handle navigation: ", e); + this.handleDriverError(e, request, "Error handling navigation"); + }; try { this.driver .navigate(request.data.uri.toString()) .catch((e: unknown) => onErr(e)) .then(() => { - return this.transport.reply( - request, - {}, - ) - }) + return this.transport.reply(request, {}); + }); } catch (e) { - return onErr(e) + return onErr(e); } } private handleOIDC(request: IGetOpenIDActionRequest): void { - let phase = 1 // 1 = initial request, 2 = after user manual confirmation + let phase = 1; // 1 = initial request, 2 = after user manual confirmation const replyState = ( state: OpenIDRequestState, credential?: IOpenIDCredentials, ): void | Promise => { - credential = credential || {} + credential = credential || {}; if (phase > 1) { return this.transport.send( WidgetApiToWidgetAction.OpenIDCredentials, @@ -485,163 +397,122 @@ export class ClientWidgetApi extends EventEmitter { original_request_id: request.requestId, ...credential, }, - ) + ); } else { - return this.transport.reply( - request, - { - state: state, - ...credential, - }, - ) + return this.transport.reply(request, { + state: state, + ...credential, + }); } - } + }; - const replyError = ( - msg: string, - ): void | Promise => { - console.error("[ClientWidgetApi] Failed to handle OIDC: ", msg) + const replyError = (msg: string): void | Promise => { + 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) + return replyState(OpenIDRequestState.Blocked); } else { - return this.transport.reply( - request, - { - error: { message: msg }, - }, - ) + 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 && 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 + replyState(update.state); + phase++; + return; } if (update.state === OpenIDRequestState.Allowed && !update.token) { - return replyError( - "client provided invalid OIDC token for an allowed request", - ) + 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 + update.token = undefined; // just in case the client did something weird } - observer.close() - return replyState(update.state, update.token) - }) + observer.close(); + return replyState(update.state, update.token); + }); - this.driver.askOpenID(observer) + this.driver.askOpenID(observer); } - private handleReadRoomAccountData( - request: IReadRoomAccountDataFromWidgetActionRequest, - ): void | Promise { - let events: Promise = Promise.resolve([]) - events = this.driver.readRoomAccountData(request.data.type) + private handleReadRoomAccountData(request: IReadRoomAccountDataFromWidgetActionRequest): void | Promise { + let events: Promise = Promise.resolve([]); + events = this.driver.readRoomAccountData(request.data.type); if (!this.canReceiveRoomAccountData(request.data.type)) { return this.transport.reply(request, { error: { message: "Cannot read room account data of this type", }, - }) + }); } return events.then((evs) => { - this.transport.reply( - request, - { events: evs }, - ) - }) + this.transport.reply(request, { events: evs }); + }); } - private async handleReadEvents( - request: IReadEventFromWidgetActionRequest, - ): Promise { + private async handleReadEvents(request: IReadEventFromWidgetActionRequest): Promise { 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) - ) { + 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[] + let askRoomIds: string[]; if (request.data.room_ids === undefined) { - askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId] + askRoomIds = this.viewedRoomId === null ? [] : [this.viewedRoomId]; } else if (request.data.room_ids === Symbols.AnyRoom) { - askRoomIds = this.driver - .getKnownRooms() - .filter((roomId) => this.canUseRoomTimeline(roomId)) + askRoomIds = this.driver.getKnownRooms().filter((roomId) => this.canUseRoomTimeline(roomId)); } else { - askRoomIds = request.data.room_ids + askRoomIds = request.data.room_ids; for (const roomId of askRoomIds) { if (!this.canUseRoomTimeline(roomId)) { - return this.transport.reply( - request, - { - error: { - message: `Unable to access room timeline: ${roomId}`, - }, + return this.transport.reply(request, { + error: { + message: `Unable to access room timeline: ${roomId}`, }, - ) + }); } } } - const limit = request.data.limit || 0 - const since = request.data.since + const limit = request.data.limit || 0; + const since = request.data.since; - let stateKey: string | undefined = undefined - let msgtype: string | undefined = undefined + let stateKey: string | undefined = undefined; + let msgtype: string | undefined = undefined; if (request.data.state_key !== undefined) { - 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", - }, + 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", }, - ) + }); } } else { - msgtype = request.data.msgtype + msgtype = request.data.msgtype; if (!this.canReceiveRoomEvent(request.data.type, msgtype)) { - return this.transport.reply( - request, - { - error: { - message: "Cannot read room events of this type", - }, + return this.transport.reply(request, { + error: { + message: "Cannot read room events of this type", }, - ) + }); } } @@ -651,86 +522,50 @@ export class ClientWidgetApi extends EventEmitter { const events = request.data.room_ids === undefined && askRoomIds.length === 0 ? await (request.data.state_key === undefined - ? this.driver.readRoomEvents( - request.data.type, - msgtype, - limit, - null, - since, - ) - : this.driver.readStateEvents( - request.data.type, - stateKey, - limit, - null, - )) + ? this.driver.readRoomEvents(request.data.type, msgtype, limit, null, since) + : this.driver.readStateEvents(request.data.type, stateKey, limit, null)) : ( await Promise.all( askRoomIds.map((roomId) => - this.driver.readRoomTimeline( - roomId, - request.data.type, - msgtype, - stateKey, - limit, - since, - ), + this.driver.readRoomTimeline(roomId, request.data.type, msgtype, stateKey, limit, since), ), ) - ).flat(1) + ).flat(1); this.transport.reply(request, { events, - }) + }); } private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { 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) - ) { + 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 isDelayedEvent = - request.data.delay !== undefined || - request.data.parent_delay_id !== undefined - if ( - isDelayedEvent && - !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent) - ) { + const isDelayedEvent = request.data.delay !== undefined || request.data.parent_delay_id !== undefined; + if (isDelayedEvent && !this.hasCapability(MatrixCapabilities.MSC4157SendDelayedEvent)) { return this.transport.reply(request, { error: { message: "Missing capability" }, - }) + }); } - let sendEventPromise: Promise< - ISendEventDetails | ISendDelayedEventDetails - > + let sendEventPromise: Promise; if (request.data.state_key !== undefined) { - if ( - !this.canSendStateEvent( - request.data.type, - request.data.state_key, - ) - ) { - return this.transport.reply( - request, - { - error: { - message: "Cannot send state events of this type", - }, + if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { + return this.transport.reply(request, { + error: { + message: "Cannot send state events of this type", }, - ) + }); } if (!isDelayedEvent) { @@ -739,7 +574,7 @@ export class ClientWidgetApi extends EventEmitter { request.data.content || {}, request.data.state_key, request.data.room_id, - ) + ); } else { sendEventPromise = this.driver.sendDelayedEvent( request.data.delay ?? null, @@ -748,20 +583,17 @@ export class ClientWidgetApi extends EventEmitter { request.data.content || {}, request.data.state_key, request.data.room_id, - ) + ); } } else { - const content = (request.data.content as { msgtype?: string }) || {} - const msgtype = content["msgtype"] + 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", - }, + return this.transport.reply(request, { + error: { + message: "Cannot send room events of this type", }, - ) + }); } if (!isDelayedEvent) { @@ -770,7 +602,7 @@ export class ClientWidgetApi extends EventEmitter { content, null, // not sending a state event request.data.room_id, - ) + ); } else { sendEventPromise = this.driver.sendDelayedEvent( request.data.delay ?? null, @@ -779,45 +611,40 @@ export class ClientWidgetApi extends EventEmitter { content, null, // not sending a state event request.data.room_id, - ) + ); } } sendEventPromise .then((sentEvent) => { - return this.transport.reply( - request, - { - room_id: sentEvent.roomId, - ...("eventId" in sentEvent - ? { - event_id: sentEvent.eventId, - } - : { - delay_id: sentEvent.delayId, - }), - }, - ) + return this.transport.reply(request, { + room_id: sentEvent.roomId, + ...("eventId" in sentEvent + ? { + event_id: sentEvent.eventId, + } + : { + delay_id: sentEvent.delayId, + }), + }); }) .catch((e: unknown) => { - console.error("error sending event: ", e) - this.handleDriverError(e, request, "Error sending event") - }) + console.error("error sending event: ", e); + this.handleDriverError(e, request, "Error sending event"); + }); } - private handleUpdateDelayedEvent( - request: IUpdateDelayedEventFromWidgetActionRequest, - ): void { + private handleUpdateDelayedEvent(request: IUpdateDelayedEventFromWidgetActionRequest): void { if (!request.data.delay_id) { return this.transport.reply(request, { error: { message: "Invalid request - missing delay_id" }, - }) + }); } if (!this.hasCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent)) { return this.transport.reply(request, { error: { message: "Missing capability" }, - }) + }); } switch (request.data.action) { @@ -825,188 +652,136 @@ export class ClientWidgetApi extends EventEmitter { case UpdateDelayedEventAction.Restart: case UpdateDelayedEventAction.Send: this.driver - .updateDelayedEvent( - request.data.delay_id, - request.data.action, - ) + .updateDelayedEvent(request.data.delay_id, request.data.action) .then(() => { - return this.transport.reply( - request, - {}, - ) + return this.transport.reply(request, {}); }) .catch((e: unknown) => { - console.error("error updating delayed event: ", e) - this.handleDriverError( - e, - request, - "Error updating delayed event", - ) - }) - break + console.error("error updating delayed event: ", e); + this.handleDriverError(e, request, "Error updating delayed event"); + }); + break; default: - return this.transport.reply( - request, - { - error: { - message: "Invalid request - unsupported action", - }, + return this.transport.reply(request, { + error: { + message: "Invalid request - unsupported action", }, - ) + }); } } - private async handleSendToDevice( - request: ISendToDeviceFromWidgetActionRequest, - ): Promise { + 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, - {}, - ) + 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) - this.handleDriverError(e, request, "Error sending event") + console.error("error sending to-device event", e); + this.handleDriverError(e, request, "Error sending event"); } } } - private async pollTurnServers( - turnServers: AsyncGenerator, - initialServer: ITurnServer, - ): Promise { + private async pollTurnServers(turnServers: AsyncGenerator, initialServer: ITurnServer): Promise { 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) + console.error("error polling for TURN servers", e); } } - private async handleWatchTurnServers( - request: IWatchTurnServersRequest, - ): Promise { + 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, - {}, - ) + await this.transport.reply(request, {}); } else { try { - const turnServers = this.driver.getTurnServers() + 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, - {}, - ) + 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 + 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" }, - }, - ) + 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 { + 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, - {}, - ) + 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, - {}, - ) + await this.turnServers.return(undefined); + this.turnServers = null; + await this.transport.reply(request, {}); } } - private async handleReadRelations( - request: IReadRelationsFromWidgetActionRequest, - ): Promise { + private async handleReadRelations(request: IReadRelationsFromWidgetActionRequest): Promise { 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) - ) { + 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 { @@ -1019,272 +794,177 @@ export class ClientWidgetApi extends EventEmitter { 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) + return this.canReceiveStateEvent(e.type, e.state_key); } else { - return this.canReceiveRoomEvent( - e.type, - (e.content as { msgtype?: string })["msgtype"], - ) + 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, - }, - ) + return this.transport.reply(request, { + chunk, + prev_batch: result.prevBatch, + next_batch: result.nextBatch, + }); } catch (e) { - console.error("error getting the relations", e) - this.handleDriverError( - e, - request, - "Unexpected error while reading relations", - ) + console.error("error getting the relations", e); + this.handleDriverError(e, request, "Unexpected error while reading relations"); } } - private async handleUserDirectorySearch( - request: IUserDirectorySearchFromWidgetActionRequest, - ): Promise { - if ( - !this.hasCapability(MatrixCapabilities.MSC3973UserDirectorySearch) - ) { + private async handleUserDirectorySearch(request: IUserDirectorySearchFromWidgetActionRequest): Promise { + 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, - })), - }, - ) + 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) - this.handleDriverError( - e, - request, - "Unexpected error while searching in the user directory", - ) + console.error("error searching in the user directory", e); + this.handleDriverError(e, request, "Unexpected error while searching in the user directory"); } } - private async handleGetMediaConfig( - request: IGetMediaConfigActionFromWidgetActionRequest, - ): Promise { + private async handleGetMediaConfig(request: IGetMediaConfigActionFromWidgetActionRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { return this.transport.reply(request, { error: { message: "Missing capability" }, - }) + }); } try { - const result = await this.driver.getMediaConfig() + const result = await this.driver.getMediaConfig(); - return this.transport.reply( - request, - result, - ) + return this.transport.reply(request, result); } catch (e) { - console.error("error while getting the media configuration", e) - this.handleDriverError( - e, - request, - "Unexpected error while getting the media configuration", - ) + console.error("error while getting the media configuration", e); + this.handleDriverError(e, request, "Unexpected error while getting the media configuration"); } } - private async handleUploadFile( - request: IUploadFileActionFromWidgetActionRequest, - ): Promise { + private async handleUploadFile(request: IUploadFileActionFromWidgetActionRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC4039UploadFile)) { return this.transport.reply(request, { error: { message: "Missing capability" }, - }) + }); } try { - const result = await this.driver.uploadFile(request.data.file) + const result = await this.driver.uploadFile(request.data.file); - return this.transport.reply( - request, - { - content_uri: result.contentUri, - }, - ) + return this.transport.reply(request, { + content_uri: result.contentUri, + }); } catch (e) { - console.error("error while uploading a file", e) - this.handleDriverError( - e, - request, - "Unexpected error while uploading a file", - ) + console.error("error while uploading a file", e); + this.handleDriverError(e, request, "Unexpected error while uploading a file"); } } - private async handleDownloadFile( - request: IDownloadFileActionFromWidgetActionRequest, - ): Promise { + private async handleDownloadFile(request: IDownloadFileActionFromWidgetActionRequest): Promise { if (!this.hasCapability(MatrixCapabilities.MSC4039DownloadFile)) { return this.transport.reply(request, { error: { message: "Missing capability" }, - }) + }); } try { - const result = await this.driver.downloadFile( - request.data.content_uri, - ) - - return this.transport.reply( - request, - { file: result.file }, - ) + const result = await this.driver.downloadFile(request.data.content_uri); + + return this.transport.reply(request, { file: result.file }); } catch (e) { - console.error("error while downloading a file", e) - this.handleDriverError( - e, - request, - "Unexpected error while downloading a file", - ) + console.error("error while downloading a file", e); + this.handleDriverError(e, request, "Unexpected error while downloading a file"); } } - private handleDriverError( - e: unknown, - request: IWidgetApiRequest, - message: string, - ): void { - const data = this.driver.processError(e) + private handleDriverError(e: unknown, request: IWidgetApiRequest, message: string): void { + const data = this.driver.processError(e); this.transport.reply(request, { error: { message, ...data, }, - }) + }); } - private handleMessage( - ev: CustomEvent, - ): void | Promise { - if (this.isStopped) return + private handleMessage(ev: CustomEvent): void | Promise { + if (this.isStopped) return; const actionEv = new CustomEvent(`action:${ev.detail.action}`, { detail: ev.detail, cancelable: true, - }) - this.emit(`action:${ev.detail.action}`, actionEv) + }); + this.emit(`action:${ev.detail.action}`, actionEv); if (!actionEv.defaultPrevented) { switch (ev.detail.action) { case WidgetApiFromWidgetAction.ContentLoaded: - return this.handleContentLoadedAction( - ev.detail, - ) + return this.handleContentLoadedAction(ev.detail); case WidgetApiFromWidgetAction.SupportedApiVersions: - return this.replyVersions( - ev.detail, - ) + return this.replyVersions(ev.detail); case WidgetApiFromWidgetAction.SendEvent: - return this.handleSendEvent( - ev.detail, - ) + return this.handleSendEvent(ev.detail); case WidgetApiFromWidgetAction.SendToDevice: - return this.handleSendToDevice( - ev.detail, - ) + return this.handleSendToDevice(ev.detail); case WidgetApiFromWidgetAction.GetOpenIDCredentials: - return this.handleOIDC(ev.detail) + return this.handleOIDC(ev.detail); case WidgetApiFromWidgetAction.MSC2931Navigate: - return this.handleNavigate( - ev.detail, - ) + return this.handleNavigate(ev.detail); case WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities: - return this.handleCapabilitiesRenegotiate( - ev.detail, - ) + return this.handleCapabilitiesRenegotiate(ev.detail); case WidgetApiFromWidgetAction.MSC2876ReadEvents: - return this.handleReadEvents( - ev.detail, - ) + return this.handleReadEvents(ev.detail); case WidgetApiFromWidgetAction.WatchTurnServers: - return this.handleWatchTurnServers( - ev.detail, - ) + return this.handleWatchTurnServers(ev.detail); case WidgetApiFromWidgetAction.UnwatchTurnServers: - return this.handleUnwatchTurnServers( - ev.detail, - ) + return this.handleUnwatchTurnServers(ev.detail); case WidgetApiFromWidgetAction.MSC3869ReadRelations: - return this.handleReadRelations( - ev.detail, - ) + return this.handleReadRelations(ev.detail); case WidgetApiFromWidgetAction.MSC3973UserDirectorySearch: - return this.handleUserDirectorySearch( - ev.detail, - ) + return this.handleUserDirectorySearch(ev.detail); case WidgetApiFromWidgetAction.BeeperReadRoomAccountData: - return this.handleReadRoomAccountData( - ev.detail, - ) + return this.handleReadRoomAccountData(ev.detail); case WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction: - return this.handleGetMediaConfig( - ev.detail, - ) + return this.handleGetMediaConfig(ev.detail); case WidgetApiFromWidgetAction.MSC4039UploadFileAction: - return this.handleUploadFile( - ev.detail, - ) + return this.handleUploadFile(ev.detail); case WidgetApiFromWidgetAction.MSC4039DownloadFileAction: - return this.handleDownloadFile( - ev.detail, - ) + return this.handleDownloadFile(ev.detail); case WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent: - return this.handleUpdateDelayedEvent( - ev.detail, - ) + return this.handleUpdateDelayedEvent(ev.detail); default: - return this.transport.reply(ev.detail, < - IWidgetApiErrorResponseData - >{ + return this.transport.reply(ev.detail, { error: { - message: - "Unknown or unsupported action: " + - ev.detail.action, + message: "Unknown or unsupported action: " + ev.detail.action, }, - }) + }); } } } @@ -1293,10 +973,8 @@ export class ClientWidgetApi extends EventEmitter { * Informs the widget that the client's theme has changed. * @param theme The theme data, as an object with arbitrary contents. */ - public updateTheme( - theme: IThemeChangeActionRequestData, - ): Promise { - return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme) + public updateTheme(theme: IThemeChangeActionRequestData): Promise { + return this.transport.send(WidgetApiToWidgetAction.ThemeChange, theme); } /** @@ -1306,7 +984,7 @@ export class ClientWidgetApi extends EventEmitter { public updateLanguage(lang: string): Promise { return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { lang, - }) + }); } /** @@ -1315,10 +993,7 @@ export class ClientWidgetApi extends EventEmitter { * @throws Throws if there is a problem. */ public takeScreenshot(): Promise { - return this.transport.send( - WidgetApiToWidgetAction.TakeScreenshot, - {}, - ) + return this.transport.send(WidgetApiToWidgetAction.TakeScreenshot, {}); } /** @@ -1326,43 +1001,24 @@ export class ClientWidgetApi extends EventEmitter { * @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, < - IVisibilityActionRequestData - >{ + 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() + return this.transport.send(WidgetApiToWidgetAction.WidgetConfig, data).then(); } - public notifyModalWidgetButtonClicked( - id: IModalWidgetOpenRequestDataButton["id"], - ): Promise { + public notifyModalWidgetButtonClicked(id: IModalWidgetOpenRequestDataButton["id"]): Promise { return this.transport - .send( - WidgetApiToWidgetAction.ButtonClicked, - { id }, - ) - .then() + .send(WidgetApiToWidgetAction.ButtonClicked, { id }) + .then(); } public notifyModalWidgetClose(data: IModalWidgetReturnData): Promise { - return this.transport - .send( - WidgetApiToWidgetAction.CloseModalWidget, - data, - ) - .then() + return this.transport.send(WidgetApiToWidgetAction.CloseModalWidget, data).then(); } /** @@ -1378,10 +1034,7 @@ export class ClientWidgetApi extends EventEmitter { * {@link ClientWidgetApi.setViewedRoomId} rather than passing it to this * method. */ - public async feedEvent( - rawEvent: IRoomEvent, - currentViewedRoomId: string, - ): Promise + public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise; /** * Feeds an event to the widget. As a client you are expected to call this * for every new event in every room to which you are joined or invited. @@ -1390,34 +1043,22 @@ export class ClientWidgetApi extends EventEmitter { * able to read the event due to permissions, rejects if the widget failed * to handle the event. */ - public async feedEvent(rawEvent: IRoomEvent): Promise - public async feedEvent( - rawEvent: IRoomEvent, - currentViewedRoomId?: string, - ): Promise { - if (currentViewedRoomId !== undefined) - this.setViewedRoomId(currentViewedRoomId) - if ( - rawEvent.room_id !== this.viewedRoomId && - !this.canUseRoomTimeline(rawEvent.room_id) - ) { - return // no-op + public async feedEvent(rawEvent: IRoomEvent): Promise; + public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId?: string): Promise { + if (currentViewedRoomId !== undefined) this.setViewedRoomId(currentViewedRoomId); + if (rawEvent.room_id !== this.viewedRoomId && !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 + return; // no-op } } else { // message event - if ( - !this.canReceiveRoomEvent( - rawEvent.type, - (rawEvent.content as { msgtype?: string })?.["msgtype"], - ) - ) { - return // no-op + if (!this.canReceiveRoomEvent(rawEvent.type, (rawEvent.content as { msgtype?: string })?.["msgtype"])) { + return; // no-op } } @@ -1426,7 +1067,7 @@ export class ClientWidgetApi extends EventEmitter { WidgetApiToWidgetAction.SendEvent, // it's compatible, but missing the index signature rawEvent as ISendEventToWidgetRequestData, - ) + ); } /** @@ -1438,56 +1079,49 @@ export class ClientWidgetApi extends EventEmitter { * able to receive the event due to permissions, rejects if the widget * failed to handle the event. */ - public async feedToDevice( - rawEvent: IRoomEvent, - encrypted: boolean, - ): Promise { + 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, - ) + ); } } - private viewedRoomId: string | null = null + private viewedRoomId: string | null = null; /** * Indicate that a room is being viewed (making it possible for the widget * to interact with it). */ public setViewedRoomId(roomId: string | null): void { - this.viewedRoomId = roomId + this.viewedRoomId = roomId; // If the widget doesn't have timeline permissions for the room then // this is its opportunity to learn the room state. We push the entire // room state, which could be redundant if this room had been viewed // once before, but it's easier than selectively pushing just the bits // of state that changed while the room was in the background. - if (roomId !== null && !this.canUseRoomTimeline(roomId)) - this.pushRoomState(roomId) + if (roomId !== null && !this.canUseRoomTimeline(roomId)) this.pushRoomState(roomId); } private async flushRoomState(): Promise { try { // Only send a single action once all concurrent tasks have completed - do await Promise.all([...this.pushRoomStateTasks]) - while (this.pushRoomStateTasks.size > 0) + do await Promise.all([...this.pushRoomStateTasks]); + while (this.pushRoomStateTasks.size > 0); - const events: IRoomEvent[] = [] + const events: IRoomEvent[] = []; for (const eventTypeMap of this.pushRoomStateResult.values()) { for (const stateKeyMap of eventTypeMap.values()) { - events.push(...stateKeyMap.values()) + events.push(...stateKeyMap.values()); } } - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { - state: events, - }, - ) + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: events, + }); } finally { - this.flushRoomStateTask = null + this.flushRoomStateTask = null; } } @@ -1497,40 +1131,26 @@ export class ClientWidgetApi extends EventEmitter { */ private pushRoomState(roomId: string): void { for (const cap of this.allowedEvents) { - if ( - cap.kind === EventKind.State && - cap.direction === EventDirection.Receive - ) { + if (cap.kind === EventKind.State && cap.direction === EventDirection.Receive) { // Initiate the task - const events = this.driver.readRoomState( - roomId, - cap.eventType, - cap.keyStr ?? undefined, - ) + const events = this.driver.readRoomState(roomId, cap.eventType, cap.keyStr ?? undefined); const task = events .then( (events) => { // When complete, queue the resulting events to be // pushed to the widget for (const event of events) { - let eventTypeMap = - this.pushRoomStateResult.get(roomId) + let eventTypeMap = this.pushRoomStateResult.get(roomId); if (eventTypeMap === undefined) { - eventTypeMap = new Map() - this.pushRoomStateResult.set( - roomId, - eventTypeMap, - ) + eventTypeMap = new Map(); + this.pushRoomStateResult.set(roomId, eventTypeMap); } - let stateKeyMap = eventTypeMap.get( - cap.eventType, - ) + let stateKeyMap = eventTypeMap.get(cap.eventType); if (stateKeyMap === undefined) { - stateKeyMap = new Map() - eventTypeMap.set(cap.eventType, stateKeyMap) + stateKeyMap = new Map(); + eventTypeMap.set(cap.eventType, stateKeyMap); } - if (!stateKeyMap.has(event.state_key!)) - stateKeyMap.set(event.state_key!, event) + if (!stateKeyMap.has(event.state_key!)) stateKeyMap.set(event.state_key!, event); } }, (e) => @@ -1541,17 +1161,15 @@ export class ClientWidgetApi extends EventEmitter { ) .then(() => { // Mark request as no longer pending - this.pushRoomStateTasks.delete(task) - }) + this.pushRoomStateTasks.delete(task); + }); // Mark task as pending - this.pushRoomStateTasks.add(task) + this.pushRoomStateTasks.add(task); // Assuming no other tasks are already happening concurrently, // schedule the widget action that actually pushes the events - this.flushRoomStateTask ??= this.flushRoomState() - this.flushRoomStateTask.catch((e) => - console.error("Failed to push room state", e), - ) + this.flushRoomStateTask ??= this.flushRoomState(); + this.flushRoomStateTask.catch((e) => console.error("Failed to push room state", e)); } } } @@ -1567,44 +1185,36 @@ export class ClientWidgetApi extends EventEmitter { widget failed to handle the update. */ public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) - throw new Error("Not a state event") + if (rawEvent.state_key === undefined) throw new Error("Not a state event"); if ( - (rawEvent.room_id === this.viewedRoomId || - this.canUseRoomTimeline(rawEvent.room_id)) && + (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) && this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) ) { // Updates could race with the initial push of the room's state if (this.pushRoomStateTasks.size === 0) { // No initial push tasks are pending; safe to send immediately - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { - state: [rawEvent], - }, - ) + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: [rawEvent], + }); } else { // Lump the update in with whatever data will be sent in the // initial push later. Even if we set it to an "outdated" entry // here, we can count on any newer entries being passed to this // same method eventually; this won't cause stuck state. - let eventTypeMap = this.pushRoomStateResult.get( - rawEvent.room_id, - ) + let eventTypeMap = this.pushRoomStateResult.get(rawEvent.room_id); if (eventTypeMap === undefined) { - eventTypeMap = new Map() - this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap) + eventTypeMap = new Map(); + this.pushRoomStateResult.set(rawEvent.room_id, eventTypeMap); } - let stateKeyMap = eventTypeMap.get(rawEvent.type) + let stateKeyMap = eventTypeMap.get(rawEvent.type); if (stateKeyMap === undefined) { - stateKeyMap = new Map() - eventTypeMap.set(rawEvent.type, stateKeyMap) + stateKeyMap = new Map(); + eventTypeMap.set(rawEvent.type, stateKeyMap); } - if (!stateKeyMap.has(rawEvent.type)) - stateKeyMap.set(rawEvent.state_key, rawEvent) - do await Promise.all([...this.pushRoomStateTasks]) - while (this.pushRoomStateTasks.size > 0) - await this.flushRoomStateTask + if (!stateKeyMap.has(rawEvent.type)) stateKeyMap.set(rawEvent.state_key, rawEvent); + do await Promise.all([...this.pushRoomStateTasks]); + while (this.pushRoomStateTasks.size > 0); + await this.flushRoomStateTask; } } } diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index e08204c..d81c8aa 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -14,53 +14,37 @@ * limitations under the License. */ -import { EventEmitter } from "events" +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 { 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" +} 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, - IWidgetApiErrorResponseDataDetails, -} from "./interfaces/IWidgetApiErrorResponse" -import { IStickerActionRequestData } from "./interfaces/StickerAction" -import { - IStickyActionRequestData, - IStickyActionResponseData, -} from "./interfaces/StickyAction" +} from "./interfaces/CapabilitiesAction"; +import { ITransport } from "./transport/ITransport"; +import { PostmessageTransport } from "./transport/PostmessageTransport"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction"; +import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails } 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" +} from "./interfaces/GetOpenIDAction"; +import { IOpenIDCredentialsActionRequest } from "./interfaces/OpenIDCredentialsAction"; +import { MatrixWidgetType, WidgetType } from "./interfaces/WidgetType"; import { BuiltInModalButtonID, IModalWidgetCreateData, @@ -68,72 +52,60 @@ import { IModalWidgetOpenRequestDataButton, IModalWidgetReturnData, ModalButtonID, -} from "./interfaces/ModalWidgetActions" -import { ISetModalButtonEnabledActionRequestData } from "./interfaces/SetModalButtonEnabledAction" -import { - ISendEventFromWidgetRequestData, - ISendEventFromWidgetResponseData, -} from "./interfaces/SendEventAction" +} 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" +} from "./interfaces/SendToDeviceAction"; +import { EventDirection, WidgetEventCapability } from "./models/WidgetEventCapability"; +import { INavigateActionRequestData } from "./interfaces/NavigateAction"; +import { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData } from "./interfaces/ReadEventAction"; import { IReadRoomAccountDataFromWidgetRequestData, IReadRoomAccountDataFromWidgetResponseData, -} from "./interfaces/ReadRoomAccountDataAction" -import { IRoomEvent } from "./interfaces/IRoomEvent" -import { IRoomAccountData } from "./interfaces/IRoomAccountData" -import { - ITurnServer, - IUpdateTurnServersRequest, -} from "./interfaces/TurnServerActions" -import { Symbols } from "./Symbols" +} from "./interfaces/ReadRoomAccountDataAction"; +import { IRoomEvent } from "./interfaces/IRoomEvent"; +import { IRoomAccountData } from "./interfaces/IRoomAccountData"; +import { ITurnServer, IUpdateTurnServersRequest } from "./interfaces/TurnServerActions"; +import { Symbols } from "./Symbols"; import { IReadRelationsFromWidgetRequestData, IReadRelationsFromWidgetResponseData, -} from "./interfaces/ReadRelationsAction" +} from "./interfaces/ReadRelationsAction"; import { IUserDirectorySearchFromWidgetRequestData, IUserDirectorySearchFromWidgetResponseData, -} from "./interfaces/UserDirectorySearchAction" +} from "./interfaces/UserDirectorySearchAction"; import { IGetMediaConfigActionFromWidgetRequestData, IGetMediaConfigActionFromWidgetResponseData, -} from "./interfaces/GetMediaConfigAction" +} from "./interfaces/GetMediaConfigAction"; import { IUploadFileActionFromWidgetRequestData, IUploadFileActionFromWidgetResponseData, -} from "./interfaces/UploadFileAction" +} from "./interfaces/UploadFileAction"; import { IDownloadFileActionFromWidgetRequestData, IDownloadFileActionFromWidgetResponseData, -} from "./interfaces/DownloadFileAction" +} from "./interfaces/DownloadFileAction"; import { IUpdateDelayedEventFromWidgetRequestData, IUpdateDelayedEventFromWidgetResponseData, UpdateDelayedEventAction, -} from "./interfaces/UpdateDelayedEventAction" +} from "./interfaces/UpdateDelayedEventAction"; export class WidgetApiResponseError extends Error { static { - this.prototype.name = this.name + this.prototype.name = this.name; } public constructor( message: string, public readonly data: IWidgetApiErrorResponseDataDetails, ) { - super(message) + super(message); } } @@ -155,14 +127,14 @@ export class WidgetApiResponseError extends Error { * can be sent and the transport will be ready. */ export class WidgetApi extends EventEmitter { - public readonly transport: ITransport + public readonly transport: ITransport; - private capabilitiesFinished = false - private supportsMSC2974Renegotiate = false - private requestedCapabilities: Capability[] = [] - private approvedCapabilities?: Capability[] - private cachedClientVersions?: ApiVersion[] - private turnServerWatchers = 0 + 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. @@ -174,20 +146,13 @@ export class WidgetApi extends EventEmitter { widgetId: string | null = null, private clientOrigin: string | null = null, ) { - super() + super(); if (!window.parent) { - throw new Error( - "No parent window. This widget doesn't appear to be embedded properly.", - ) + 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)) + this.transport = new PostmessageTransport(WidgetApiDirection.FromWidget, widgetId, window.parent, window); + this.transport.targetOrigin = clientOrigin; + this.transport.on("message", this.handleMessage.bind(this)); } /** @@ -199,9 +164,9 @@ export class WidgetApi extends EventEmitter { */ public hasCapability(capability: Capability): boolean { if (Array.isArray(this.approvedCapabilities)) { - return this.approvedCapabilities.includes(capability) + return this.approvedCapabilities.includes(capability); } - return this.requestedCapabilities.includes(capability) + return this.requestedCapabilities.includes(capability); } /** @@ -213,10 +178,10 @@ export class WidgetApi extends EventEmitter { */ public requestCapability(capability: Capability): void { if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) { - throw new Error("Capabilities have already been negotiated") + throw new Error("Capabilities have already been negotiated"); } - this.requestedCapabilities.push(capability) + this.requestedCapabilities.push(capability); } /** @@ -226,7 +191,7 @@ export class WidgetApi extends EventEmitter { * @throws Throws if the capabilities negotiation has already started. */ public requestCapabilities(capabilities: Capability[]): void { - capabilities.forEach((cap) => this.requestCapability(cap)) + capabilities.forEach((cap) => this.requestCapability(cap)); } /** @@ -235,10 +200,8 @@ export class WidgetApi extends EventEmitter { * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` to * denote all known rooms. */ - public requestCapabilityForRoomTimeline( - roomId: string | Symbols.AnyRoom, - ): void { - this.requestCapability(`org.matrix.msc2762.timeline:${roomId}`) + public requestCapabilityForRoomTimeline(roomId: string | Symbols.AnyRoom): void { + this.requestCapability(`org.matrix.msc2762.timeline:${roomId}`); } /** @@ -249,17 +212,8 @@ export class WidgetApi extends EventEmitter { * @param {string} stateKey If specified, the specific state key to request. * Otherwise all state keys will be requested. */ - public requestCapabilityToSendState( - eventType: string, - stateKey?: string, - ): void { - this.requestCapability( - WidgetEventCapability.forStateEvent( - EventDirection.Send, - eventType, - stateKey, - ).raw, - ) + public requestCapabilityToSendState(eventType: string, stateKey?: string): void { + this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Send, eventType, stateKey).raw); } /** @@ -270,17 +224,8 @@ export class WidgetApi extends EventEmitter { * @param {string} stateKey If specified, the specific state key to request. * Otherwise all state keys will be requested. */ - public requestCapabilityToReceiveState( - eventType: string, - stateKey?: string, - ): void { - this.requestCapability( - WidgetEventCapability.forStateEvent( - EventDirection.Receive, - eventType, - stateKey, - ).raw, - ) + public requestCapabilityToReceiveState(eventType: string, stateKey?: string): void { + this.requestCapability(WidgetEventCapability.forStateEvent(EventDirection.Receive, eventType, stateKey).raw); } /** @@ -290,12 +235,7 @@ export class WidgetApi extends EventEmitter { * @param {string} eventType The room event type to ask for. */ public requestCapabilityToSendToDevice(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forToDeviceEvent( - EventDirection.Send, - eventType, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Send, eventType).raw); } /** @@ -305,12 +245,7 @@ export class WidgetApi extends EventEmitter { * @param {string} eventType The room event type to ask for. */ public requestCapabilityToReceiveToDevice(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forToDeviceEvent( - EventDirection.Receive, - eventType, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forToDeviceEvent(EventDirection.Receive, eventType).raw); } /** @@ -319,10 +254,7 @@ export class WidgetApi extends EventEmitter { * @param {string} eventType The room event type to ask for. */ public requestCapabilityToSendEvent(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType) - .raw, - ) + this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType).raw); } /** @@ -331,12 +263,7 @@ export class WidgetApi extends EventEmitter { * @param {string} eventType The room event type to ask for. */ public requestCapabilityToReceiveEvent(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forRoomEvent( - EventDirection.Receive, - eventType, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw); } /** @@ -347,12 +274,7 @@ export class WidgetApi extends EventEmitter { * Otherwise all message types will be requested. */ public requestCapabilityToSendMessage(msgtype?: string): void { - this.requestCapability( - WidgetEventCapability.forRoomMessageEvent( - EventDirection.Send, - msgtype, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Send, msgtype).raw); } /** @@ -363,12 +285,7 @@ export class WidgetApi extends EventEmitter { * Otherwise all message types will be requested. */ public requestCapabilityToReceiveMessage(msgtype?: string): void { - this.requestCapability( - WidgetEventCapability.forRoomMessageEvent( - EventDirection.Receive, - msgtype, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forRoomMessageEvent(EventDirection.Receive, msgtype).raw); } /** @@ -377,12 +294,7 @@ export class WidgetApi extends EventEmitter { * @param {string} eventType The state event type to ask for. */ public requestCapabilityToReceiveRoomAccountData(eventType: string): void { - this.requestCapability( - WidgetEventCapability.forRoomAccountData( - EventDirection.Receive, - eventType, - ).raw, - ) + this.requestCapability(WidgetEventCapability.forRoomAccountData(EventDirection.Receive, eventType).raw); } /** @@ -396,84 +308,44 @@ export class WidgetApi extends EventEmitter { public requestOpenIDConnectToken(): Promise { return new Promise((resolve, reject) => { this.transport - .sendComplete< - IGetOpenIDActionRequestData, - IGetOpenIDActionResponse - >(WidgetApiFromWidgetAction.GetOpenIDCredentials, {}) + .sendComplete( + WidgetApiFromWidgetAction.GetOpenIDCredentials, + {}, + ) .then((response) => { - const rdata = response.response + const rdata = response.response; if (rdata.state === OpenIDRequestState.Allowed) { - resolve(rdata) + 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, - ): void => { - 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, - {}, - ) // ack - } else if ( - request.data.state === - OpenIDRequestState.Blocked - ) { - reject( - new Error( - "User declined to verify their identity", - ), - ) - this.transport.reply( - request, - {}, - ) // ack + reject(new Error("User declined to verify their identity")); + } else if (rdata.state === OpenIDRequestState.PendingUserConfirmation) { + const handlerFn = (ev: CustomEvent): void => { + 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, {}); // ack + } else if (request.data.state === OpenIDRequestState.Blocked) { + reject(new Error("User declined to verify their identity")); + this.transport.reply(request, {}); // ack } else { - reject( - new Error( - "Invalid state on reply: " + - rdata.state, - ), - ) - this.transport.reply(request, < - IWidgetApiErrorResponseData - >{ + reject(new Error("Invalid state on reply: " + rdata.state)); + this.transport.reply(request, { error: { message: "Invalid state", }, - }) + }); } - this.off( - `action:${WidgetApiToWidgetAction.OpenIDCredentials}`, - handlerFn, - ) - } - this.on( - `action:${WidgetApiToWidgetAction.OpenIDCredentials}`, - handlerFn, - ) + this.off(`action:${WidgetApiToWidgetAction.OpenIDCredentials}`, handlerFn); + }; + this.on(`action:${WidgetApiToWidgetAction.OpenIDCredentials}`, handlerFn); } else { - reject(new Error("Invalid state: " + rdata.state)) + reject(new Error("Invalid state: " + rdata.state)); } }) - .catch(reject) - }) + .catch(reject); + }); } /** @@ -485,12 +357,10 @@ export class WidgetApi extends EventEmitter { */ public updateRequestedCapabilities(): Promise { return this.transport - .send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, < - IRenegotiateCapabilitiesRequestData - >{ + .send(WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, { capabilities: this.requestedCapabilities, }) - .then() + .then(); } /** @@ -498,12 +368,7 @@ export class WidgetApi extends EventEmitter { * @returns {Promise} Resolves when the client acknowledges the request. */ public sendContentLoaded(): Promise { - return this.transport - .send( - WidgetApiFromWidgetAction.ContentLoaded, - {}, - ) - .then() + return this.transport.send(WidgetApiFromWidgetAction.ContentLoaded, {}).then(); } /** @@ -512,9 +377,7 @@ export class WidgetApi extends EventEmitter { * @returns {Promise} Resolves when the client acknowledges the request. */ public sendSticker(sticker: IStickerActionRequestData): Promise { - return this.transport - .send(WidgetApiFromWidgetAction.SendSticker, sticker) - .then() + return this.transport.send(WidgetApiFromWidgetAction.SendSticker, sticker).then(); } /** @@ -529,7 +392,7 @@ export class WidgetApi extends EventEmitter { IStickyActionRequestData, IStickyActionResponseData >(WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, { value }) - .then((res) => res.success) + .then((res) => res.success); } /** @@ -549,17 +412,14 @@ export class WidgetApi extends EventEmitter { type: WidgetType = MatrixWidgetType.Custom, ): Promise { return this.transport - .send( - WidgetApiFromWidgetAction.OpenModalWidget, - { - type, - url, - name, - buttons, - data, - }, - ) - .then() + .send(WidgetApiFromWidgetAction.OpenModalWidget, { + type, + url, + name, + buttons, + data, + }) + .then(); } /** @@ -568,12 +428,7 @@ export class WidgetApi extends EventEmitter { * @returns {Promise} Resolves when complete. */ public closeModalWidget(data: IModalWidgetReturnData = {}): Promise { - return this.transport - .send( - WidgetApiFromWidgetAction.CloseModalWidget, - data, - ) - .then() + return this.transport.send(WidgetApiFromWidgetAction.CloseModalWidget, data).then(); } public sendRoomEvent( @@ -583,14 +438,7 @@ export class WidgetApi extends EventEmitter { delay?: number, parentDelayId?: string, ): Promise { - return this.sendEvent( - eventType, - undefined, - content, - roomId, - delay, - parentDelayId, - ) + return this.sendEvent(eventType, undefined, content, roomId, delay, parentDelayId); } public sendStateEvent( @@ -601,14 +449,7 @@ export class WidgetApi extends EventEmitter { delay?: number, parentDelayId?: string, ): Promise { - return this.sendEvent( - eventType, - stateKey, - content, - roomId, - delay, - parentDelayId, - ) + return this.sendEvent(eventType, stateKey, content, roomId, delay, parentDelayId); } private sendEvent( @@ -619,19 +460,19 @@ export class WidgetApi extends EventEmitter { delay?: number, parentDelayId?: string, ): Promise { - return this.transport.send< - ISendEventFromWidgetRequestData, - ISendEventFromWidgetResponseData - >(WidgetApiFromWidgetAction.SendEvent, { - type: eventType, - content, - ...(stateKey !== undefined && { state_key: stateKey }), - ...(roomId !== undefined && { room_id: roomId }), - ...(delay !== undefined && { delay }), - ...(parentDelayId !== undefined && { - parent_delay_id: parentDelayId, - }), - }) + return this.transport.send( + WidgetApiFromWidgetAction.SendEvent, + { + type: eventType, + content, + ...(stateKey !== undefined && { state_key: stateKey }), + ...(roomId !== undefined && { room_id: roomId }), + ...(delay !== undefined && { delay }), + ...(parentDelayId !== undefined && { + parent_delay_id: parentDelayId, + }), + }, + ); } /** @@ -641,13 +482,13 @@ export class WidgetApi extends EventEmitter { delayId: string, action: UpdateDelayedEventAction, ): Promise { - return this.transport.send< - IUpdateDelayedEventFromWidgetRequestData, - IUpdateDelayedEventFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, { - delay_id: delayId, - action, - }) + return this.transport.send( + WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent, + { + delay_id: delayId, + action, + }, + ); } /** @@ -662,27 +503,24 @@ export class WidgetApi extends EventEmitter { encrypted: boolean, contentMap: { [userId: string]: { [deviceId: string]: object } }, ): Promise { - return this.transport.send< - ISendToDeviceFromWidgetRequestData, - ISendToDeviceFromWidgetResponseData - >(WidgetApiFromWidgetAction.SendToDevice, { - type: eventType, - encrypted, - messages: contentMap, - }) + return this.transport.send( + WidgetApiFromWidgetAction.SendToDevice, + { + type: eventType, + encrypted, + messages: contentMap, + }, + ); } - public readRoomAccountData( - eventType: string, - roomIds?: (string | Symbols.AnyRoom)[], - ): Promise { - const data: IReadEventFromWidgetRequestData = { type: eventType } + public readRoomAccountData(eventType: string, roomIds?: (string | Symbols.AnyRoom)[]): Promise { + const data: IReadEventFromWidgetRequestData = { type: eventType }; if (roomIds) { if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom + data.room_ids = Symbols.AnyRoom; } else { - data.room_ids = roomIds + data.room_ids = roomIds; } } return this.transport @@ -690,7 +528,7 @@ export class WidgetApi extends EventEmitter { IReadRoomAccountDataFromWidgetRequestData, IReadRoomAccountDataFromWidgetResponseData >(WidgetApiFromWidgetAction.BeeperReadRoomAccountData, data) - .then((r) => r.events) + .then((r) => r.events); } public readRoomEvents( @@ -703,26 +541,26 @@ export class WidgetApi extends EventEmitter { const data: IReadEventFromWidgetRequestData = { type: eventType, msgtype: msgtype, - } + }; if (limit !== undefined) { - data.limit = limit + data.limit = limit; } if (roomIds) { if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom + data.room_ids = Symbols.AnyRoom; } else { - data.room_ids = roomIds + data.room_ids = roomIds; } } if (since) { - data.since = since + data.since = since; } return this.transport .send< IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData >(WidgetApiFromWidgetAction.MSC2876ReadEvents, data) - .then((r) => r.events) + .then((r) => r.events); } /** @@ -754,11 +592,9 @@ export class WidgetApi extends EventEmitter { to?: string, direction?: "f" | "b", ): Promise { - const versions = await this.getClientVersions() + const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC3869)) { - throw new Error( - "The read_relations action is not supported by the client.", - ) + throw new Error("The read_relations action is not supported by the client."); } const data: IReadRelationsFromWidgetRequestData = { @@ -770,12 +606,12 @@ export class WidgetApi extends EventEmitter { from, limit, direction, - } + }; - return this.transport.send< - IReadRelationsFromWidgetRequestData, - IReadRelationsFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC3869ReadRelations, data) + return this.transport.send( + WidgetApiFromWidgetAction.MSC3869ReadRelations, + data, + ); } public readStateEvents( @@ -787,15 +623,15 @@ export class WidgetApi extends EventEmitter { const data: IReadEventFromWidgetRequestData = { type: eventType, state_key: stateKey === undefined ? true : stateKey, - } + }; if (limit !== undefined) { - data.limit = limit + data.limit = limit; } if (roomIds) { if (roomIds.includes(Symbols.AnyRoom)) { - data.room_ids = Symbols.AnyRoom + data.room_ids = Symbols.AnyRoom; } else { - data.room_ids = roomIds + data.room_ids = roomIds; } } return this.transport @@ -803,7 +639,7 @@ export class WidgetApi extends EventEmitter { IReadEventFromWidgetRequestData, IReadEventFromWidgetResponseData >(WidgetApiFromWidgetAction.MSC2876ReadEvents, data) - .then((r) => r.events) + .then((r) => r.events); } /** @@ -813,22 +649,16 @@ export class WidgetApi extends EventEmitter { * @returns {Promise} 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 { + public setModalButtonEnabled(buttonId: ModalButtonID, isEnabled: boolean): Promise { if (buttonId === BuiltInModalButtonID.Close) { - throw new Error("The close button cannot be disabled") + throw new Error("The close button cannot be disabled"); } return this.transport - .send( - WidgetApiFromWidgetAction.SetModalButtonEnabled, - { - button: buttonId, - enabled: isEnabled, - }, - ) - .then() + .send(WidgetApiFromWidgetAction.SetModalButtonEnabled, { + button: buttonId, + enabled: isEnabled, + }) + .then(); } /** @@ -841,15 +671,12 @@ export class WidgetApi extends EventEmitter { */ public navigateTo(uri: string): Promise { if (!uri || !uri.startsWith("https://matrix.to/#")) { - throw new Error("Invalid matrix.to URI") + throw new Error("Invalid matrix.to URI"); } return this.transport - .send( - WidgetApiFromWidgetAction.MSC2931Navigate, - { uri }, - ) - .then() + .send(WidgetApiFromWidgetAction.MSC2931Navigate, { uri }) + .then(); } /** @@ -858,65 +685,43 @@ export class WidgetApi extends EventEmitter { * @yields {ITurnServer} The TURN server URIs and credentials currently available to the widget. */ public async *getTurnServers(): AsyncGenerator { - let setTurnServer: (server: ITurnServer) => void - - const onUpdateTurnServers = async ( - ev: CustomEvent, - ): Promise => { - ev.preventDefault() - setTurnServer(ev.detail.data) - await this.transport.reply( - ev.detail, - {}, - ) - } + let setTurnServer: (server: ITurnServer) => void; + + const onUpdateTurnServers = async (ev: CustomEvent): Promise => { + ev.preventDefault(); + setTurnServer(ev.detail.data); + await this.transport.reply(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, - ) + 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( - WidgetApiFromWidgetAction.WatchTurnServers, - {}, - ) + await this.transport.send(WidgetApiFromWidgetAction.WatchTurnServers, {}); } catch (e) { - this.off( - `action:${WidgetApiToWidgetAction.UpdateTurnServers}`, - onUpdateTurnServers, - ) - throw e + this.off(`action:${WidgetApiToWidgetAction.UpdateTurnServers}`, onUpdateTurnServers); + throw e; } } - this.turnServerWatchers++ + this.turnServerWatchers++; try { // Watch for new data indefinitely (until this generator's return method is called) while (true) { - yield await new Promise( - (resolve) => (setTurnServer = resolve), - ) + yield await new Promise((resolve) => (setTurnServer = resolve)); } } finally { // The loop was broken by the caller - clean up - this.off( - `action:${WidgetApiToWidgetAction.UpdateTurnServers}`, - onUpdateTurnServers, - ) + 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-- + this.turnServerWatchers--; if (this.turnServerWatchers === 0) { - await this.transport.send( - WidgetApiFromWidgetAction.UnwatchTurnServers, - {}, - ) + await this.transport.send(WidgetApiFromWidgetAction.UnwatchTurnServers, {}); } } } @@ -931,22 +736,20 @@ export class WidgetApi extends EventEmitter { searchTerm: string, limit?: number, ): Promise { - const versions = await this.getClientVersions() + const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC3973)) { - throw new Error( - "The user_directory_search action is not supported by the client.", - ) + 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) + >(WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data); } /** @@ -954,19 +757,17 @@ export class WidgetApi extends EventEmitter { * @returns Promise which resolves with an object containing the config. */ public async getMediaConfig(): Promise { - const versions = await this.getClientVersions() + const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error( - "The get_media_config action is not supported by the client.", - ) + throw new Error("The get_media_config action is not supported by the client."); } - const data: IGetMediaConfigActionFromWidgetRequestData = {} + const data: IGetMediaConfigActionFromWidgetRequestData = {}; return this.transport.send< IGetMediaConfigActionFromWidgetRequestData, IGetMediaConfigActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data) + >(WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data); } /** @@ -975,24 +776,20 @@ export class WidgetApi extends EventEmitter { * XMLHttpRequest.send (typically a File). * @returns Resolves to the location of the uploaded file. */ - public async uploadFile( - file: XMLHttpRequestBodyInit, - ): Promise { - const versions = await this.getClientVersions() + public async uploadFile(file: XMLHttpRequestBodyInit): Promise { + const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error( - "The upload_file action is not supported by the client.", - ) + throw new Error("The upload_file action is not supported by the client."); } const data: IUploadFileActionFromWidgetRequestData = { file, - } + }; - return this.transport.send< - IUploadFileActionFromWidgetRequestData, - IUploadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039UploadFileAction, data) + return this.transport.send( + WidgetApiFromWidgetAction.MSC4039UploadFileAction, + data, + ); } /** @@ -1000,24 +797,20 @@ export class WidgetApi extends EventEmitter { * @param contentUri - MXC URI of the file to download. * @returns Resolves to the contents of the file. */ - public async downloadFile( - contentUri: string, - ): Promise { - const versions = await this.getClientVersions() + public async downloadFile(contentUri: string): Promise { + const versions = await this.getClientVersions(); if (!versions.includes(UnstableApiVersion.MSC4039)) { - throw new Error( - "The download_file action is not supported by the client.", - ) + throw new Error("The download_file action is not supported by the client."); } const data: IDownloadFileActionFromWidgetRequestData = { content_uri: contentUri, - } + }; - return this.transport.send< - IDownloadFileActionFromWidgetRequestData, - IDownloadFileActionFromWidgetResponseData - >(WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data) + return this.transport.send( + WidgetApiFromWidgetAction.MSC4039DownloadFileAction, + data, + ); } /** @@ -1025,52 +818,36 @@ export class WidgetApi extends EventEmitter { * that messages are not missed. Communication can only be stopped by the client. */ public start(): void { - this.transport.start() + this.transport.start(); this.getClientVersions().then((v) => { if (v.includes(UnstableApiVersion.MSC2974)) { - this.supportsMSC2974Renegotiate = true + this.supportsMSC2974Renegotiate = true; } - }) + }); } - private handleMessage( - ev: CustomEvent, - ): void | Promise { + private handleMessage(ev: CustomEvent): void | Promise { const actionEv = new CustomEvent(`action:${ev.detail.action}`, { detail: ev.detail, cancelable: true, - }) - this.emit(`action:${ev.detail.action}`, actionEv) + }); + this.emit(`action:${ev.detail.action}`, actionEv); if (!actionEv.defaultPrevented) { switch (ev.detail.action) { case WidgetApiToWidgetAction.SupportedApiVersions: - return this.replyVersions( - ev.detail, - ) + return this.replyVersions(ev.detail); case WidgetApiToWidgetAction.Capabilities: - return this.handleCapabilities( - ev.detail, - ) + return this.handleCapabilities(ev.detail); case WidgetApiToWidgetAction.UpdateVisibility: - return this.transport.reply( - ev.detail, - {}, - ) // ack to avoid error spam + return this.transport.reply(ev.detail, {}); // ack to avoid error spam case WidgetApiToWidgetAction.NotifyCapabilities: - return this.transport.reply( - ev.detail, - {}, - ) // ack to avoid error spam + return this.transport.reply(ev.detail, {}); // ack to avoid error spam default: - return this.transport.reply(ev.detail, < - IWidgetApiErrorResponseData - >{ + return this.transport.reply(ev.detail, { error: { - message: - "Unknown or unsupported action: " + - ev.detail.action, + message: "Unknown or unsupported action: " + ev.detail.action, }, - }) + }); } } } @@ -1078,41 +855,36 @@ export class WidgetApi extends EventEmitter { private replyVersions(request: ISupportedVersionsActionRequest): void { this.transport.reply(request, { supported_versions: CurrentApiVersions, - }) + }); } public getClientVersions(): Promise { if (Array.isArray(this.cachedClientVersions)) { - return Promise.resolve(this.cachedClientVersions) + return Promise.resolve(this.cachedClientVersions); } return this.transport - .send< - IWidgetApiRequestEmptyData, - ISupportedVersionsActionResponseData - >(WidgetApiFromWidgetAction.SupportedApiVersions, {}) + .send( + WidgetApiFromWidgetAction.SupportedApiVersions, + {}, + ) .then((r) => { - this.cachedClientVersions = r.supported_versions - return r.supported_versions + this.cachedClientVersions = r.supported_versions; + return r.supported_versions; }) .catch((e) => { - console.warn( - "non-fatal error getting supported client versions: ", - e, - ) - return [] - }) + console.warn("non-fatal error getting supported client versions: ", e); + return []; + }); } - private handleCapabilities( - request: ICapabilitiesActionRequest, - ): void | Promise { + private handleCapabilities(request: ICapabilitiesActionRequest): void | Promise { if (this.capabilitiesFinished) { return this.transport.reply(request, { error: { message: "Capability negotiation already completed", }, - }) + }); } // See if we can expect a capabilities notification or not @@ -1121,23 +893,20 @@ export class WidgetApi extends EventEmitter { this.once( `action:${WidgetApiToWidgetAction.NotifyCapabilities}`, (ev: CustomEvent) => { - this.approvedCapabilities = ev.detail.data.approved - this.emit("ready") + 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") + this.emit("ready"); } // in either case, reply to that capabilities request - this.capabilitiesFinished = true - return this.transport.reply( - request, - { - capabilities: this.requestedCapabilities, - }, - ) - }) + this.capabilitiesFinished = true; + return this.transport.reply(request, { + capabilities: this.requestedCapabilities, + }); + }); } } diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index 20fa098..df92c03 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -24,41 +24,41 @@ import { ITurnServer, IWidgetApiErrorResponseDataDetails, UpdateDelayedEventAction, -} from ".." +} from ".."; export interface ISendEventDetails { - roomId: string - eventId: string + roomId: string; + eventId: string; } export interface ISendDelayedEventDetails { - roomId: string - delayId: string + roomId: string; + delayId: string; } export interface IOpenIDUpdate { - state: OpenIDRequestState - token?: IOpenIDCredentials + state: OpenIDRequestState; + token?: IOpenIDCredentials; } export interface IReadEventRelationsResult { - chunk: IRoomEvent[] - nextBatch?: string - prevBatch?: string + chunk: IRoomEvent[]; + nextBatch?: string; + prevBatch?: string; } export interface ISearchUserDirectoryResult { - limited: boolean + limited: boolean; results: Array<{ - userId: string - displayName?: string - avatarUrl?: string - }> + userId: string; + displayName?: string; + avatarUrl?: string; + }>; } export interface IGetMediaConfigResult { - [key: string]: unknown - "m.upload.size"?: number + [key: string]: unknown; + "m.upload.size"?: number; } /** @@ -83,10 +83,8 @@ export abstract class WidgetDriver { * @param {Set} requested The set of requested capabilities. * @returns {Promise>} Resolves to the allowed capabilities. */ - public validateCapabilities( - requested: Set, - ): Promise> { - return Promise.resolve(new Set()) + public validateCapabilities(requested: Set): Promise> { + return Promise.resolve(new Set()); } /** @@ -109,7 +107,7 @@ export abstract class WidgetDriver { stateKey: string | null = null, roomId: string | null = null, ): Promise { - return Promise.reject(new Error("Failed to override function")) + return Promise.reject(new Error("Failed to override function")); } /** @@ -139,7 +137,7 @@ export abstract class WidgetDriver { stateKey: string | null = null, roomId: string | null = null, ): Promise { - return Promise.reject(new Error("Failed to override function")) + return Promise.reject(new Error("Failed to override function")); } /** @@ -147,11 +145,8 @@ export abstract class WidgetDriver { * Run the specified {@link action} for the delayed event matching the provided {@link delayId}. * @throws Rejected when there is no matching delayed event, or when the action failed to run. */ - public updateDelayedEvent( - delayId: string, - action: UpdateDelayedEventAction, - ): Promise { - return Promise.reject(new Error("Failed to override function")) + public updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { + return Promise.reject(new Error("Failed to override function")); } /** @@ -168,7 +163,7 @@ export abstract class WidgetDriver { encrypted: boolean, contentMap: { [userId: string]: { [deviceId: string]: object } }, ): Promise { - return Promise.reject(new Error("Failed to override function")) + return Promise.reject(new Error("Failed to override function")); } /** * Reads an element of room account data. The widget API will have already verified that the widget is @@ -180,11 +175,8 @@ export abstract class WidgetDriver { * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. * @returns {Promise} Resolves to the element of room account data, or an empty array. */ - public readRoomAccountData( - eventType: string, - roomIds: string[] | null = null, - ): Promise { - return Promise.resolve([]) + public readRoomAccountData(eventType: string, roomIds: string[] | null = null): Promise { + return Promise.resolve([]); } /** @@ -216,7 +208,7 @@ export abstract class WidgetDriver { roomIds: string[] | null = null, since?: string, ): Promise { - return Promise.resolve([]) + return Promise.resolve([]); } /** @@ -241,7 +233,7 @@ export abstract class WidgetDriver { limit: number, roomIds: string[] | null = null, ): Promise { - return Promise.resolve([]) + return Promise.resolve([]); } /** @@ -270,15 +262,8 @@ export abstract class WidgetDriver { ): Promise { // For backward compatibility we try the deprecated methods, in case // they're implemented - if (stateKey === undefined) - return this.readRoomEvents( - eventType, - msgtype, - limit, - [roomId], - since, - ) - else return this.readStateEvents(eventType, stateKey, limit, [roomId]) + if (stateKey === undefined) return this.readRoomEvents(eventType, msgtype, limit, [roomId], since); + else return this.readStateEvents(eventType, stateKey, limit, [roomId]); } /** @@ -290,12 +275,8 @@ export abstract class WidgetDriver { * @returns {Promise} Resolves to the events representing the * current values of the room state entries. */ - public readRoomState( - roomId: string, - eventType: string, - stateKey: string | undefined, - ): Promise { - return Promise.resolve([]) + public readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise { + return Promise.resolve([]); } /** @@ -332,7 +313,7 @@ export abstract class WidgetDriver { limit?: number, direction?: "f" | "b", ): Promise { - return Promise.resolve({ chunk: [] }) + return Promise.resolve({ chunk: [] }); } /** @@ -349,7 +330,7 @@ export abstract class WidgetDriver { * @param {SimpleObservable} observer The observable to feed updates into. */ public askOpenID(observer: SimpleObservable): void { - observer.update({ state: OpenIDRequestState.Blocked }) + observer.update({ state: OpenIDRequestState.Blocked }); } /** @@ -362,7 +343,7 @@ export abstract class WidgetDriver { * @throws Throws if there's a problem with the navigation, such as invalid format. */ public navigate(uri: string): Promise { - throw new Error("Navigation is not implemented") + throw new Error("Navigation is not implemented"); } /** @@ -372,7 +353,7 @@ export abstract class WidgetDriver { * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. */ public getTurnServers(): AsyncGenerator { - throw new Error("TURN server support is not implemented") + throw new Error("TURN server support is not implemented"); } /** @@ -381,11 +362,8 @@ export abstract class WidgetDriver { * @param limit The maximum number of results to return. If not supplied, the * @returns Resolves to the search results. */ - public searchUserDirectory( - searchTerm: string, - limit?: number, - ): Promise { - return Promise.resolve({ limited: false, results: [] }) + public searchUserDirectory(searchTerm: string, limit?: number): Promise { + return Promise.resolve({ limited: false, results: [] }); } /** @@ -393,7 +371,7 @@ export abstract class WidgetDriver { * @returns Promise which resolves with an object containing the config. */ public getMediaConfig(): Promise { - throw new Error("Get media config is not implemented") + throw new Error("Get media config is not implemented"); } /** @@ -402,10 +380,8 @@ export abstract class WidgetDriver { * XMLHttpRequest.send (typically a File). * @returns Resolves to the location of the uploaded file. */ - public uploadFile( - file: XMLHttpRequestBodyInit, - ): Promise<{ contentUri: string }> { - throw new Error("Upload file is not implemented") + public uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }> { + throw new Error("Upload file is not implemented"); } /** @@ -413,10 +389,8 @@ export abstract class WidgetDriver { * @param contentUri - MXC URI of the file to download. * @returns Resolves to the contents of the file. */ - public downloadFile( - contentUri: string, - ): Promise<{ file: XMLHttpRequestBodyInit }> { - throw new Error("Download file is not implemented") + public downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }> { + throw new Error("Download file is not implemented"); } /** @@ -425,7 +399,7 @@ export abstract class WidgetDriver { * @returns The room IDs. */ public getKnownRooms(): string[] { - throw new Error("Querying known rooms is not implemented") + throw new Error("Querying known rooms is not implemented"); } /** @@ -434,9 +408,7 @@ export abstract class WidgetDriver { * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, * or undefined if it cannot be expressed as one. */ - public processError( - error: unknown, - ): IWidgetApiErrorResponseDataDetails | undefined { - return undefined + public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined { + return undefined; } } diff --git a/src/index.ts b/src/index.ts index 3194b27..bfdff11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,67 +15,67 @@ limitations under the License. */ // Primary structures -export * from "./WidgetApi" -export * from "./ClientWidgetApi" -export * from "./Symbols" +export * from "./WidgetApi"; +export * from "./ClientWidgetApi"; +export * from "./Symbols"; // Transports (not sure why you'd use these directly, but might as well export all the things) -export type * from "./transport/ITransport" -export * from "./transport/PostmessageTransport" +export type * from "./transport/ITransport"; +export * from "./transport/PostmessageTransport"; // Interfaces and simple models -export type * from "./interfaces/ICustomWidgetData" -export type * from "./interfaces/IJitsiWidgetData" -export type * from "./interfaces/IStickerpickerWidgetData" -export type * from "./interfaces/IWidget" -export * from "./interfaces/WidgetType" -export * from "./interfaces/IWidgetApiErrorResponse" -export type * from "./interfaces/IWidgetApiRequest" -export type * from "./interfaces/IWidgetApiResponse" -export * from "./interfaces/WidgetApiAction" -export * from "./interfaces/WidgetApiDirection" -export * from "./interfaces/ApiVersion" -export * from "./interfaces/Capabilities" -export type * from "./interfaces/CapabilitiesAction" -export type * from "./interfaces/ContentLoadedAction" -export type * from "./interfaces/ScreenshotAction" -export type * from "./interfaces/StickerAction" -export type * from "./interfaces/StickyAction" -export type * from "./interfaces/SupportedVersionsAction" -export type * from "./interfaces/VisibilityAction" -export * from "./interfaces/GetOpenIDAction" -export type * from "./interfaces/OpenIDCredentialsAction" -export * from "./interfaces/WidgetKind" -export * from "./interfaces/ModalButtonKind" -export * from "./interfaces/ModalWidgetActions" -export type * from "./interfaces/SetModalButtonEnabledAction" -export type * from "./interfaces/WidgetConfigAction" -export type * from "./interfaces/SendEventAction" -export type * from "./interfaces/SendToDeviceAction" -export type * from "./interfaces/ReadEventAction" -export type * from "./interfaces/IRoomEvent" -export type * from "./interfaces/IRoomAccountData" -export type * from "./interfaces/NavigateAction" -export type * from "./interfaces/TurnServerActions" -export type * from "./interfaces/ReadRelationsAction" -export type * from "./interfaces/GetMediaConfigAction" -export * from "./interfaces/UpdateDelayedEventAction" -export type * from "./interfaces/UpdateStateAction" -export type * from "./interfaces/UploadFileAction" -export type * from "./interfaces/DownloadFileAction" -export type * from "./interfaces/ThemeChangeAction" -export type * from "./interfaces/LanguageChangeAction" +export type * from "./interfaces/ICustomWidgetData"; +export type * from "./interfaces/IJitsiWidgetData"; +export type * from "./interfaces/IStickerpickerWidgetData"; +export type * from "./interfaces/IWidget"; +export * from "./interfaces/WidgetType"; +export * from "./interfaces/IWidgetApiErrorResponse"; +export type * from "./interfaces/IWidgetApiRequest"; +export type * from "./interfaces/IWidgetApiResponse"; +export * from "./interfaces/WidgetApiAction"; +export * from "./interfaces/WidgetApiDirection"; +export * from "./interfaces/ApiVersion"; +export * from "./interfaces/Capabilities"; +export type * from "./interfaces/CapabilitiesAction"; +export type * from "./interfaces/ContentLoadedAction"; +export type * from "./interfaces/ScreenshotAction"; +export type * from "./interfaces/StickerAction"; +export type * from "./interfaces/StickyAction"; +export type * from "./interfaces/SupportedVersionsAction"; +export type * from "./interfaces/VisibilityAction"; +export * from "./interfaces/GetOpenIDAction"; +export type * from "./interfaces/OpenIDCredentialsAction"; +export * from "./interfaces/WidgetKind"; +export * from "./interfaces/ModalButtonKind"; +export * from "./interfaces/ModalWidgetActions"; +export type * from "./interfaces/SetModalButtonEnabledAction"; +export type * from "./interfaces/WidgetConfigAction"; +export type * from "./interfaces/SendEventAction"; +export type * from "./interfaces/SendToDeviceAction"; +export type * from "./interfaces/ReadEventAction"; +export type * from "./interfaces/IRoomEvent"; +export type * from "./interfaces/IRoomAccountData"; +export type * from "./interfaces/NavigateAction"; +export type * from "./interfaces/TurnServerActions"; +export type * from "./interfaces/ReadRelationsAction"; +export type * from "./interfaces/GetMediaConfigAction"; +export * from "./interfaces/UpdateDelayedEventAction"; +export type * from "./interfaces/UpdateStateAction"; +export type * from "./interfaces/UploadFileAction"; +export type * from "./interfaces/DownloadFileAction"; +export type * from "./interfaces/ThemeChangeAction"; +export type * from "./interfaces/LanguageChangeAction"; // Complex models -export * from "./models/WidgetEventCapability" -export * from "./models/validation/url" -export * from "./models/validation/utils" -export * from "./models/Widget" -export * from "./models/WidgetParser" +export * from "./models/WidgetEventCapability"; +export * from "./models/validation/url"; +export * from "./models/validation/utils"; +export * from "./models/Widget"; +export * from "./models/WidgetParser"; // Utilities -export * from "./templating/url-template" -export * from "./util/SimpleObservable" +export * from "./templating/url-template"; +export * from "./util/SimpleObservable"; // Drivers -export * from "./driver/WidgetDriver" +export * from "./driver/WidgetDriver"; diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index 388d633..ab0546e 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -34,7 +34,7 @@ export enum UnstableApiVersion { MSC4039 = "org.matrix.msc4039", } -export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string +export type ApiVersion = MatrixApiVersion | UnstableApiVersion | string; export const CurrentApiVersions: ApiVersion[] = [ MatrixApiVersion.Prerelease1, @@ -51,4 +51,4 @@ export const CurrentApiVersions: ApiVersion[] = [ UnstableApiVersion.MSC3869, UnstableApiVersion.MSC3973, UnstableApiVersion.MSC4039, -] +]; diff --git a/src/interfaces/Capabilities.ts b/src/interfaces/Capabilities.ts index df414d4..f541ac5 100644 --- a/src/interfaces/Capabilities.ts +++ b/src/interfaces/Capabilities.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Symbols } from "../Symbols" +import { Symbols } from "../Symbols"; export enum MatrixCapabilities { Screenshots = "m.capability.screenshot", @@ -52,14 +52,10 @@ export enum MatrixCapabilities { MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } -export type Capability = MatrixCapabilities | string +export type Capability = MatrixCapabilities | string; -export const StickerpickerCapabilities: Capability[] = [ - MatrixCapabilities.StickerSending, -] -export const VideoConferenceCapabilities: Capability[] = [ - MatrixCapabilities.AlwaysOnScreen, -] +export const StickerpickerCapabilities: Capability[] = [MatrixCapabilities.StickerSending]; +export const VideoConferenceCapabilities: Capability[] = [MatrixCapabilities.AlwaysOnScreen]; /** * Determines if a capability is a capability for a timeline. @@ -68,7 +64,7 @@ export const VideoConferenceCapabilities: Capability[] = [ */ export function isTimelineCapability(capability: Capability): boolean { // TODO: Change when MSC2762 becomes stable. - return capability?.startsWith("org.matrix.msc2762.timeline:") + return capability?.startsWith("org.matrix.msc2762.timeline:"); } /** @@ -77,11 +73,8 @@ export function isTimelineCapability(capability: Capability): boolean { * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` for that designation. * @returns {boolean} True if a matching capability, false otherwise. */ -export function isTimelineCapabilityFor( - capability: Capability, - roomId: string | Symbols.AnyRoom, -): boolean { - return capability === `org.matrix.msc2762.timeline:${roomId}` +export function isTimelineCapabilityFor(capability: Capability, roomId: string | Symbols.AnyRoom): boolean { + return capability === `org.matrix.msc2762.timeline:${roomId}`; } /** @@ -89,8 +82,6 @@ export function isTimelineCapabilityFor( * @param {string} capability The capability to parse. * @returns {string} The room ID. */ -export function getTimelineRoomIDFromCapability( - capability: Capability, -): string { - return capability.substring(capability.indexOf(":") + 1) +export function getTimelineRoomIDFromCapability(capability: Capability): string { + return capability.substring(capability.indexOf(":") + 1); } diff --git a/src/interfaces/CapabilitiesAction.ts b/src/interfaces/CapabilitiesAction.ts index 7022d17..365bb79 100644 --- a/src/interfaces/CapabilitiesAction.ts +++ b/src/interfaces/CapabilitiesAction.ts @@ -14,64 +14,47 @@ * limitations under the License. */ -import { - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { Capability } from "./Capabilities" -import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponseData, -} from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { Capability } from "./Capabilities"; +import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface ICapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.Capabilities - data: IWidgetApiRequestEmptyData + action: WidgetApiToWidgetAction.Capabilities; + data: IWidgetApiRequestEmptyData; } -export interface ICapabilitiesActionResponseData - extends IWidgetApiResponseData { - capabilities: Capability[] +export interface ICapabilitiesActionResponseData extends IWidgetApiResponseData { + capabilities: Capability[]; } -export interface ICapabilitiesActionResponse - extends ICapabilitiesActionRequest { - response: ICapabilitiesActionResponseData +export interface ICapabilitiesActionResponse extends ICapabilitiesActionRequest { + response: ICapabilitiesActionResponseData; } -export interface INotifyCapabilitiesActionRequestData - extends IWidgetApiRequestData { - requested: Capability[] - approved: Capability[] +export interface INotifyCapabilitiesActionRequestData extends IWidgetApiRequestData { + requested: Capability[]; + approved: Capability[]; } export interface INotifyCapabilitiesActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.NotifyCapabilities - data: INotifyCapabilitiesActionRequestData + action: WidgetApiToWidgetAction.NotifyCapabilities; + data: INotifyCapabilitiesActionRequestData; } -export interface INotifyCapabilitiesActionResponse - extends INotifyCapabilitiesActionRequest { - response: IWidgetApiAcknowledgeResponseData +export interface INotifyCapabilitiesActionResponse extends INotifyCapabilitiesActionRequest { + response: IWidgetApiAcknowledgeResponseData; } -export interface IRenegotiateCapabilitiesActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities - data: IRenegotiateCapabilitiesRequestData +export interface IRenegotiateCapabilitiesActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities; + data: IRenegotiateCapabilitiesRequestData; } -export interface IRenegotiateCapabilitiesRequestData - extends IWidgetApiResponseData { - capabilities: Capability[] +export interface IRenegotiateCapabilitiesRequestData extends IWidgetApiResponseData { + capabilities: Capability[]; } -export interface IRenegotiateCapabilitiesActionResponse - extends IRenegotiateCapabilitiesActionRequest { +export interface IRenegotiateCapabilitiesActionResponse extends IRenegotiateCapabilitiesActionRequest { // nothing } diff --git a/src/interfaces/ContentLoadedAction.ts b/src/interfaces/ContentLoadedAction.ts index 709aba2..ceca93f 100644 --- a/src/interfaces/ContentLoadedAction.ts +++ b/src/interfaces/ContentLoadedAction.ts @@ -14,19 +14,15 @@ * limitations under the License. */ -import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IContentLoadedActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.ContentLoaded - data: IWidgetApiRequestEmptyData + action: WidgetApiFromWidgetAction.ContentLoaded; + data: IWidgetApiRequestEmptyData; } -export interface IContentLoadedActionResponse - extends IContentLoadedActionRequest { - response: IWidgetApiAcknowledgeResponseData +export interface IContentLoadedActionResponse extends IContentLoadedActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/DownloadFileAction.ts b/src/interfaces/DownloadFileAction.ts index 3b0f540..f3eed2e 100644 --- a/src/interfaces/DownloadFileAction.ts +++ b/src/interfaces/DownloadFileAction.ts @@ -14,27 +14,23 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IDownloadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - content_uri: string // eslint-disable-line camelcase +export interface IDownloadFileActionFromWidgetRequestData extends IWidgetApiRequestData { + content_uri: string; // eslint-disable-line camelcase } -export interface IDownloadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction - data: IDownloadFileActionFromWidgetRequestData +export interface IDownloadFileActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction; + data: IDownloadFileActionFromWidgetRequestData; } -export interface IDownloadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - file: XMLHttpRequestBodyInit +export interface IDownloadFileActionFromWidgetResponseData extends IWidgetApiResponseData { + file: XMLHttpRequestBodyInit; } -export interface IDownloadFileActionFromWidgetActionResponse - extends IDownloadFileActionFromWidgetActionRequest { - response: IDownloadFileActionFromWidgetResponseData +export interface IDownloadFileActionFromWidgetActionResponse extends IDownloadFileActionFromWidgetActionRequest { + response: IDownloadFileActionFromWidgetResponseData; } diff --git a/src/interfaces/GetMediaConfigAction.ts b/src/interfaces/GetMediaConfigAction.ts index 2ce036e..71f19d1 100644 --- a/src/interfaces/GetMediaConfigAction.ts +++ b/src/interfaces/GetMediaConfigAction.ts @@ -14,25 +14,21 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IGetMediaConfigActionFromWidgetRequestData - extends IWidgetApiRequestData {} +export interface IGetMediaConfigActionFromWidgetRequestData extends IWidgetApiRequestData {} -export interface IGetMediaConfigActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction - data: IGetMediaConfigActionFromWidgetRequestData +export interface IGetMediaConfigActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction; + data: IGetMediaConfigActionFromWidgetRequestData; } -export interface IGetMediaConfigActionFromWidgetResponseData - extends IWidgetApiResponseData { - "m.upload.size"?: number +export interface IGetMediaConfigActionFromWidgetResponseData extends IWidgetApiResponseData { + "m.upload.size"?: number; } -export interface IGetMediaConfigActionFromWidgetActionResponse - extends IGetMediaConfigActionFromWidgetActionRequest { - response: IGetMediaConfigActionFromWidgetResponseData +export interface IGetMediaConfigActionFromWidgetActionResponse extends IGetMediaConfigActionFromWidgetActionRequest { + response: IGetMediaConfigActionFromWidgetResponseData; } diff --git a/src/interfaces/GetOpenIDAction.ts b/src/interfaces/GetOpenIDAction.ts index e673846..000313c 100644 --- a/src/interfaces/GetOpenIDAction.ts +++ b/src/interfaces/GetOpenIDAction.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export enum OpenIDRequestState { Allowed = "allowed", @@ -25,10 +25,10 @@ export enum OpenIDRequestState { } export interface IOpenIDCredentials { - access_token?: string // eslint-disable-line camelcase - expires_in?: number // eslint-disable-line camelcase - matrix_server_name?: string // eslint-disable-line camelcase - token_type?: "Bearer" | string // eslint-disable-line camelcase + access_token?: string; // eslint-disable-line camelcase + expires_in?: number; // eslint-disable-line camelcase + matrix_server_name?: string; // eslint-disable-line camelcase + token_type?: "Bearer" | string; // eslint-disable-line camelcase } export interface IGetOpenIDActionRequestData extends IWidgetApiRequestData { @@ -36,16 +36,14 @@ export interface IGetOpenIDActionRequestData extends IWidgetApiRequestData { } export interface IGetOpenIDActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.GetOpenIDCredentials - data: IGetOpenIDActionRequestData + action: WidgetApiFromWidgetAction.GetOpenIDCredentials; + data: IGetOpenIDActionRequestData; } -export interface IGetOpenIDActionResponseData - extends IWidgetApiResponseData, - IOpenIDCredentials { - state: OpenIDRequestState +export interface IGetOpenIDActionResponseData extends IWidgetApiResponseData, IOpenIDCredentials { + state: OpenIDRequestState; } export interface IGetOpenIDActionResponse extends IGetOpenIDActionRequest { - response: IGetOpenIDActionResponseData + response: IGetOpenIDActionResponseData; } diff --git a/src/interfaces/ICustomWidgetData.ts b/src/interfaces/ICustomWidgetData.ts index a3d93e3..56657fb 100644 --- a/src/interfaces/ICustomWidgetData.ts +++ b/src/interfaces/ICustomWidgetData.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget" +import { IWidgetData } from "./IWidget"; /** * Widget data for m.custom specifically. @@ -23,5 +23,5 @@ export interface ICustomWidgetData extends IWidgetData { /** * The URL for the widget if the templated URL is not exactly what will be loaded. */ - url?: string + url?: string; } diff --git a/src/interfaces/IJitsiWidgetData.ts b/src/interfaces/IJitsiWidgetData.ts index 3764594..65b22a0 100644 --- a/src/interfaces/IJitsiWidgetData.ts +++ b/src/interfaces/IJitsiWidgetData.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget" +import { IWidgetData } from "./IWidget"; /** * Widget data for m.jitsi widgets. @@ -23,16 +23,16 @@ export interface IJitsiWidgetData extends IWidgetData { /** * The domain where the Jitsi Meet conference is being held. */ - domain: string + domain: string; /** * The conference ID (also known as the room name) where the conference is being held. */ - conferenceId: string + conferenceId: string; /** * Optional. True to indicate that the conference should be without video, false * otherwise (default). */ - isAudioOnly?: boolean + isAudioOnly?: boolean; } diff --git a/src/interfaces/IRoomAccountData.ts b/src/interfaces/IRoomAccountData.ts index 18682df..750bdef 100644 --- a/src/interfaces/IRoomAccountData.ts +++ b/src/interfaces/IRoomAccountData.ts @@ -15,7 +15,7 @@ */ export interface IRoomAccountData { - type: string - room_id: string // eslint-disable-line camelcase - content: unknown + type: string; + room_id: string; // eslint-disable-line camelcase + content: unknown; } diff --git a/src/interfaces/IRoomEvent.ts b/src/interfaces/IRoomEvent.ts index 1d90f53..5e90005 100644 --- a/src/interfaces/IRoomEvent.ts +++ b/src/interfaces/IRoomEvent.ts @@ -15,12 +15,12 @@ */ export interface IRoomEvent { - type: string - sender: string - event_id: string // eslint-disable-line camelcase - room_id: string // eslint-disable-line camelcase - state_key?: string // eslint-disable-line camelcase - origin_server_ts: number // eslint-disable-line camelcase - content: unknown - unsigned: unknown + type: string; + sender: string; + event_id: string; // eslint-disable-line camelcase + room_id: string; // eslint-disable-line camelcase + state_key?: string; // eslint-disable-line camelcase + origin_server_ts: number; // eslint-disable-line camelcase + content: unknown; + unsigned: unknown; } diff --git a/src/interfaces/IStickerpickerWidgetData.ts b/src/interfaces/IStickerpickerWidgetData.ts index 729b22b..1459fa5 100644 --- a/src/interfaces/IStickerpickerWidgetData.ts +++ b/src/interfaces/IStickerpickerWidgetData.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IWidgetData } from "./IWidget" +import { IWidgetData } from "./IWidget"; export interface IStickerpickerWidgetData extends IWidgetData { // no additional properties (for now) diff --git a/src/interfaces/IWidget.ts b/src/interfaces/IWidget.ts index d6e7f98..a6ee670 100644 --- a/src/interfaces/IWidget.ts +++ b/src/interfaces/IWidget.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WidgetType } from "./WidgetType" +import { WidgetType } from "./WidgetType"; /** * Widget data. @@ -23,12 +23,12 @@ export interface IWidgetData { /** * Optional title for the widget. */ - title?: string + title?: string; /** * Custom keys for inclusion in the template URL. */ - [key: string]: unknown + [key: string]: unknown; } /** @@ -39,37 +39,37 @@ export interface IWidget { /** * The ID of the widget. */ - id: string + id: string; /** * The user ID who originally created the widget. */ - creatorUserId: string + creatorUserId: string; /** * Optional name for the widget. */ - name?: string + name?: string; /** * The type of widget. */ - type: WidgetType + type: WidgetType; /** * The URL for the widget, with template variables. */ - url: string + url: string; /** * Optional flag to indicate whether or not the client should initiate communication * right after the iframe loads (default, true) or when the widget indicates it is * ready (false). */ - waitForIframeLoad?: boolean + waitForIframeLoad?: boolean; /** * Data for the widget. */ - data?: IWidgetData + data?: IWidgetData; } diff --git a/src/interfaces/IWidgetApiErrorResponse.ts b/src/interfaces/IWidgetApiErrorResponse.ts index ee92482..a215c2a 100644 --- a/src/interfaces/IWidgetApiErrorResponse.ts +++ b/src/interfaces/IWidgetApiErrorResponse.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - IWidgetApiResponse, - IWidgetApiResponseData, -} from "./IWidgetApiResponse" +import { IWidgetApiResponse, IWidgetApiResponseData } from "./IWidgetApiResponse"; /** * The format of errors returned by Matrix API requests @@ -25,42 +22,35 @@ import { */ export interface IMatrixApiError { /** The HTTP status code of the associated request. */ - http_status: number // eslint-disable-line camelcase + http_status: number; // eslint-disable-line camelcase /** Any HTTP response headers that are relevant to the error. */ - http_headers: { [name: string]: string } // eslint-disable-line camelcase + http_headers: { [name: string]: string }; // eslint-disable-line camelcase /** The URL of the failed request. */ - url: string + url: string; /** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */ response: { - errcode: string - error: string - } & IWidgetApiResponseData // extensible + errcode: string; + error: string; + } & IWidgetApiResponseData; // extensible } export interface IWidgetApiErrorResponseDataDetails { /** Set if the error came from a Matrix API request made by a widget driver */ - matrix_api_error?: IMatrixApiError // eslint-disable-line camelcase + matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase } export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData { error: { /** A user-friendly string describing the error */ - message: string - } & IWidgetApiErrorResponseDataDetails + message: string; + } & IWidgetApiErrorResponseDataDetails; } export interface IWidgetApiErrorResponse extends IWidgetApiResponse { - response: IWidgetApiErrorResponseData + response: IWidgetApiErrorResponseData; } -export function isErrorResponse( - responseData: IWidgetApiResponseData, -): responseData is IWidgetApiErrorResponseData { - const error = responseData.error - return ( - typeof error === "object" && - error !== null && - "message" in error && - typeof error.message === "string" - ) +export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData { + const error = responseData.error; + return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"; } diff --git a/src/interfaces/IWidgetApiRequest.ts b/src/interfaces/IWidgetApiRequest.ts index 8af787e..f783630 100644 --- a/src/interfaces/IWidgetApiRequest.ts +++ b/src/interfaces/IWidgetApiRequest.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { WidgetApiDirection } from "./WidgetApiDirection" -import { WidgetApiAction } from "./WidgetApiAction" +import { WidgetApiDirection } from "./WidgetApiDirection"; +import { WidgetApiAction } from "./WidgetApiAction"; export interface IWidgetApiRequestData { - [key: string]: unknown + [key: string]: unknown; } export interface IWidgetApiRequestEmptyData extends IWidgetApiRequestData { @@ -26,13 +26,13 @@ export interface IWidgetApiRequestEmptyData extends IWidgetApiRequestData { } export interface IWidgetApiRequest { - api: WidgetApiDirection - requestId: string - action: WidgetApiAction - widgetId: string - data: IWidgetApiRequestData + api: WidgetApiDirection; + requestId: string; + action: WidgetApiAction; + widgetId: string; + data: IWidgetApiRequestData; // XXX: This is for Scalar support // TODO: Fix scalar // eslint-disable-next-line @typescript-eslint/no-explicit-any - visible?: any + visible?: any; } diff --git a/src/interfaces/IWidgetApiResponse.ts b/src/interfaces/IWidgetApiResponse.ts index e7b63ff..2347b6f 100644 --- a/src/interfaces/IWidgetApiResponse.ts +++ b/src/interfaces/IWidgetApiResponse.ts @@ -14,17 +14,16 @@ * limitations under the License. */ -import { IWidgetApiRequest } from "./IWidgetApiRequest" +import { IWidgetApiRequest } from "./IWidgetApiRequest"; export interface IWidgetApiResponseData { - [key: string]: unknown + [key: string]: unknown; } -export interface IWidgetApiAcknowledgeResponseData - extends IWidgetApiResponseData { +export interface IWidgetApiAcknowledgeResponseData extends IWidgetApiResponseData { // nothing } export interface IWidgetApiResponse extends IWidgetApiRequest { - response: IWidgetApiResponseData + response: IWidgetApiResponseData; } diff --git a/src/interfaces/LanguageChangeAction.ts b/src/interfaces/LanguageChangeAction.ts index 593d6f4..8b5de3a 100644 --- a/src/interfaces/LanguageChangeAction.ts +++ b/src/interfaces/LanguageChangeAction.ts @@ -14,24 +14,22 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; -export interface ILanguageChangeActionRequestData - extends IWidgetApiRequestData { +export interface ILanguageChangeActionRequestData extends IWidgetApiRequestData { /** * The BCP 47 identifier for the client's current language. */ - lang: string + lang: string; } export interface ILanguageChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.LanguageChange - data: ILanguageChangeActionRequestData + action: WidgetApiToWidgetAction.LanguageChange; + data: ILanguageChangeActionRequestData; } -export interface ILanguageChangeActionResponse - extends ILanguageChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData +export interface ILanguageChangeActionResponse extends ILanguageChangeActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/ModalWidgetActions.ts b/src/interfaces/ModalWidgetActions.ts index 319d49e..b8f07d4 100644 --- a/src/interfaces/ModalWidgetActions.ts +++ b/src/interfaces/ModalWidgetActions.ts @@ -14,87 +14,76 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse" -import { IWidget } from "./IWidget" -import { ModalButtonKind } from "./ModalButtonKind" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; +import { IWidget } from "./IWidget"; +import { ModalButtonKind } from "./ModalButtonKind"; export enum BuiltInModalButtonID { Close = "m.close", } -export type ModalButtonID = BuiltInModalButtonID | string +export type ModalButtonID = BuiltInModalButtonID | string; export interface IModalWidgetCreateData extends IWidgetApiRequestData { - [key: string]: unknown + [key: string]: unknown; } export interface IModalWidgetReturnData { - [key: string]: unknown + [key: string]: unknown; } // Types for a normal modal requesting the opening a modal widget export interface IModalWidgetOpenRequestDataButton { - id: ModalButtonID - label: string - kind: ModalButtonKind | string - disabled?: boolean + id: ModalButtonID; + label: string; + kind: ModalButtonKind | string; + disabled?: boolean; } -export interface IModalWidgetOpenRequestData - extends IModalWidgetCreateData, - Omit { - buttons?: IModalWidgetOpenRequestDataButton[] +export interface IModalWidgetOpenRequestData extends IModalWidgetCreateData, Omit { + buttons?: IModalWidgetOpenRequestDataButton[]; } export interface IModalWidgetOpenRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.OpenModalWidget - data: IModalWidgetOpenRequestData + action: WidgetApiFromWidgetAction.OpenModalWidget; + data: IModalWidgetOpenRequestData; } export interface IModalWidgetOpenResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } // Types for a modal widget receiving notifications that its buttons have been pressed -export interface IModalWidgetButtonClickedRequestData - extends IWidgetApiRequestData { - id: IModalWidgetOpenRequestDataButton["id"] +export interface IModalWidgetButtonClickedRequestData extends IWidgetApiRequestData { + id: IModalWidgetOpenRequestDataButton["id"]; } export interface IModalWidgetButtonClickedRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ButtonClicked - data: IModalWidgetButtonClickedRequestData + action: WidgetApiToWidgetAction.ButtonClicked; + data: IModalWidgetButtonClickedRequestData; } export interface IModalWidgetButtonClickedResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } // Types for a modal widget requesting close export interface IModalWidgetCloseRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.CloseModalWidget - data: IModalWidgetReturnData + action: WidgetApiFromWidgetAction.CloseModalWidget; + data: IModalWidgetReturnData; } export interface IModalWidgetCloseResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } // Types for a normal widget being notified that the modal widget it opened has been closed -export interface IModalWidgetCloseNotificationRequest - extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.CloseModalWidget - data: IModalWidgetReturnData +export interface IModalWidgetCloseNotificationRequest extends IWidgetApiRequest { + action: WidgetApiToWidgetAction.CloseModalWidget; + data: IModalWidgetReturnData; } -export interface IModalWidgetCloseNotificationResponse - extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData +export interface IModalWidgetCloseNotificationResponse extends IWidgetApiResponse { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/NavigateAction.ts b/src/interfaces/NavigateAction.ts index fc23a64..04960eb 100644 --- a/src/interfaces/NavigateAction.ts +++ b/src/interfaces/NavigateAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface INavigateActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2931Navigate - data: INavigateActionRequestData + action: WidgetApiFromWidgetAction.MSC2931Navigate; + data: INavigateActionRequestData; } export interface INavigateActionRequestData extends IWidgetApiRequestData { - uri: string + uri: string; } export interface INavigateActionResponse extends INavigateActionRequest { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/OpenIDCredentialsAction.ts b/src/interfaces/OpenIDCredentialsAction.ts index d0adb41..c4766f1 100644 --- a/src/interfaces/OpenIDCredentialsAction.ts +++ b/src/interfaces/OpenIDCredentialsAction.ts @@ -14,29 +14,25 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IOpenIDCredentials, OpenIDRequestState } from "./GetOpenIDAction" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IOpenIDCredentials, OpenIDRequestState } from "./GetOpenIDAction"; -export interface IOpenIDCredentialsActionRequestData - extends IWidgetApiRequestData, - IOpenIDCredentials { - state: OpenIDRequestState - original_request_id: string // eslint-disable-line camelcase +export interface IOpenIDCredentialsActionRequestData extends IWidgetApiRequestData, IOpenIDCredentials { + state: OpenIDRequestState; + original_request_id: string; // eslint-disable-line camelcase } export interface IOpenIDCredentialsActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.OpenIDCredentials - data: IOpenIDCredentialsActionRequestData + action: WidgetApiToWidgetAction.OpenIDCredentials; + data: IOpenIDCredentialsActionRequestData; } -export interface IOpenIDCredentialsActionResponseData - extends IWidgetApiResponseData { +export interface IOpenIDCredentialsActionResponseData extends IWidgetApiResponseData { // nothing } -export interface IOpenIDCredentialsIDActionResponse - extends IOpenIDCredentialsActionRequest { - response: IOpenIDCredentialsActionResponseData +export interface IOpenIDCredentialsIDActionResponse extends IOpenIDCredentialsActionRequest { + response: IOpenIDCredentialsActionResponseData; } diff --git a/src/interfaces/ReadEventAction.ts b/src/interfaces/ReadEventAction.ts index f969235..3176989 100644 --- a/src/interfaces/ReadEventAction.ts +++ b/src/interfaces/ReadEventAction.ts @@ -14,32 +14,30 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IRoomEvent } from "./IRoomEvent" -import { Symbols } from "../Symbols" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IRoomEvent } from "./IRoomEvent"; +import { Symbols } from "../Symbols"; export interface IReadEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string | boolean // eslint-disable-line camelcase - msgtype?: string - type: string - limit?: number - room_ids?: Symbols.AnyRoom | string[] // eslint-disable-line camelcase - since?: string + state_key?: string | boolean; // eslint-disable-line camelcase + msgtype?: string; + type: string; + limit?: number; + room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase + since?: string; } export interface IReadEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC2876ReadEvents - data: IReadEventFromWidgetRequestData + action: WidgetApiFromWidgetAction.MSC2876ReadEvents; + data: IReadEventFromWidgetRequestData; } -export interface IReadEventFromWidgetResponseData - extends IWidgetApiResponseData { - events: IRoomEvent[] +export interface IReadEventFromWidgetResponseData extends IWidgetApiResponseData { + events: IRoomEvent[]; } -export interface IReadEventFromWidgetActionResponse - extends IReadEventFromWidgetActionRequest { - response: IReadEventFromWidgetResponseData +export interface IReadEventFromWidgetActionResponse extends IReadEventFromWidgetActionRequest { + response: IReadEventFromWidgetResponseData; } diff --git a/src/interfaces/ReadRelationsAction.ts b/src/interfaces/ReadRelationsAction.ts index 3fa3772..d89d538 100644 --- a/src/interfaces/ReadRelationsAction.ts +++ b/src/interfaces/ReadRelationsAction.ts @@ -14,39 +14,35 @@ * limitations under the License. */ -import { IRoomEvent } from "./IRoomEvent" -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IRoomEvent } from "./IRoomEvent"; +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IReadRelationsFromWidgetRequestData - extends IWidgetApiRequestData { - event_id: string // eslint-disable-line camelcase - rel_type?: string // eslint-disable-line camelcase - event_type?: string // eslint-disable-line camelcase - room_id?: string // eslint-disable-line camelcase +export interface IReadRelationsFromWidgetRequestData extends IWidgetApiRequestData { + event_id: string; // eslint-disable-line camelcase + rel_type?: string; // eslint-disable-line camelcase + event_type?: string; // eslint-disable-line camelcase + room_id?: string; // eslint-disable-line camelcase - limit?: number - from?: string - to?: string - direction?: "f" | "b" + limit?: number; + from?: string; + to?: string; + direction?: "f" | "b"; } -export interface IReadRelationsFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3869ReadRelations - data: IReadRelationsFromWidgetRequestData +export interface IReadRelationsFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3869ReadRelations; + data: IReadRelationsFromWidgetRequestData; } -export interface IReadRelationsFromWidgetResponseData - extends IWidgetApiResponseData { - chunk: IRoomEvent[] +export interface IReadRelationsFromWidgetResponseData extends IWidgetApiResponseData { + chunk: IRoomEvent[]; - next_batch?: string // eslint-disable-line camelcase - prev_batch?: string // eslint-disable-line camelcase + next_batch?: string; // eslint-disable-line camelcase + prev_batch?: string; // eslint-disable-line camelcase } -export interface IReadRelationsFromWidgetActionResponse - extends IReadRelationsFromWidgetActionRequest { - response: IReadRelationsFromWidgetResponseData +export interface IReadRelationsFromWidgetActionResponse extends IReadRelationsFromWidgetActionRequest { + response: IReadRelationsFromWidgetResponseData; } diff --git a/src/interfaces/ReadRoomAccountDataAction.ts b/src/interfaces/ReadRoomAccountDataAction.ts index 743eba1..15c1201 100644 --- a/src/interfaces/ReadRoomAccountDataAction.ts +++ b/src/interfaces/ReadRoomAccountDataAction.ts @@ -14,30 +14,26 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IRoomAccountData } from "./IRoomAccountData" -import { Symbols } from "../Symbols" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IRoomAccountData } from "./IRoomAccountData"; +import { Symbols } from "../Symbols"; -export interface IReadRoomAccountDataFromWidgetRequestData - extends IWidgetApiRequestData { - type: string - room_ids?: Symbols.AnyRoom | string[] // eslint-disable-line camelcase +export interface IReadRoomAccountDataFromWidgetRequestData extends IWidgetApiRequestData { + type: string; + room_ids?: Symbols.AnyRoom | string[]; // eslint-disable-line camelcase } -export interface IReadRoomAccountDataFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData - data: IReadRoomAccountDataFromWidgetRequestData +export interface IReadRoomAccountDataFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.BeeperReadRoomAccountData; + data: IReadRoomAccountDataFromWidgetRequestData; } -export interface IReadRoomAccountDataFromWidgetResponseData - extends IWidgetApiResponseData { - events: IRoomAccountData[] +export interface IReadRoomAccountDataFromWidgetResponseData extends IWidgetApiResponseData { + events: IRoomAccountData[]; } -export interface IReadRoomAccountDataFromWidgetActionResponse - extends IReadRoomAccountDataFromWidgetActionRequest { - response: IReadRoomAccountDataFromWidgetResponseData +export interface IReadRoomAccountDataFromWidgetActionResponse extends IReadRoomAccountDataFromWidgetActionRequest { + response: IReadRoomAccountDataFromWidgetResponseData; } diff --git a/src/interfaces/ScreenshotAction.ts b/src/interfaces/ScreenshotAction.ts index 96a0644..f9ec315 100644 --- a/src/interfaces/ScreenshotAction.ts +++ b/src/interfaces/ScreenshotAction.ts @@ -14,22 +14,19 @@ * limitations under the License. */ -import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface IScreenshotActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.TakeScreenshot - data: IWidgetApiRequestEmptyData + action: WidgetApiToWidgetAction.TakeScreenshot; + data: IWidgetApiRequestEmptyData; } export interface IScreenshotActionResponseData extends IWidgetApiResponseData { - screenshot: Blob + screenshot: Blob; } export interface IScreenshotActionResponse extends IScreenshotActionRequest { - response: IScreenshotActionResponseData + response: IScreenshotActionResponseData; } diff --git a/src/interfaces/SendEventAction.ts b/src/interfaces/SendEventAction.ts index a4daa9d..4631dac 100644 --- a/src/interfaces/SendEventAction.ts +++ b/src/interfaces/SendEventAction.ts @@ -14,58 +14,50 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IRoomEvent } from "./IRoomEvent" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IRoomEvent } from "./IRoomEvent"; export interface ISendEventFromWidgetRequestData extends IWidgetApiRequestData { - state_key?: string // eslint-disable-line camelcase - type: string - content: unknown - room_id?: string // eslint-disable-line camelcase + state_key?: string; // eslint-disable-line camelcase + type: string; + content: unknown; + room_id?: string; // eslint-disable-line camelcase // MSC4157 - delay?: number // eslint-disable-line camelcase - parent_delay_id?: string // eslint-disable-line camelcase + delay?: number; // eslint-disable-line camelcase + parent_delay_id?: string; // eslint-disable-line camelcase } export interface ISendEventFromWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendEvent - data: ISendEventFromWidgetRequestData + action: WidgetApiFromWidgetAction.SendEvent; + data: ISendEventFromWidgetRequestData; } -export interface ISendEventFromWidgetResponseData - extends IWidgetApiResponseData { - room_id: string // eslint-disable-line camelcase - event_id?: string // eslint-disable-line camelcase +export interface ISendEventFromWidgetResponseData extends IWidgetApiResponseData { + room_id: string; // eslint-disable-line camelcase + event_id?: string; // eslint-disable-line camelcase // MSC4157 - delay_id?: string // eslint-disable-line camelcase + delay_id?: string; // eslint-disable-line camelcase } -export interface ISendEventFromWidgetActionResponse - extends ISendEventFromWidgetActionRequest { - response: ISendEventFromWidgetResponseData +export interface ISendEventFromWidgetActionResponse extends ISendEventFromWidgetActionRequest { + response: ISendEventFromWidgetResponseData; } -export interface ISendEventToWidgetRequestData - extends IWidgetApiRequestData, - IRoomEvent {} +export interface ISendEventToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent {} export interface ISendEventToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendEvent - data: ISendEventToWidgetRequestData + action: WidgetApiToWidgetAction.SendEvent; + data: ISendEventToWidgetRequestData; } export interface ISendEventToWidgetResponseData extends IWidgetApiResponseData { // nothing } -export interface ISendEventToWidgetActionResponse - extends ISendEventToWidgetActionRequest { - response: ISendEventToWidgetResponseData +export interface ISendEventToWidgetActionResponse extends ISendEventToWidgetActionRequest { + response: ISendEventToWidgetResponseData; } diff --git a/src/interfaces/SendToDeviceAction.ts b/src/interfaces/SendToDeviceAction.ts index 2a01528..e7507b3 100644 --- a/src/interfaces/SendToDeviceAction.ts +++ b/src/interfaces/SendToDeviceAction.ts @@ -14,54 +14,43 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IRoomEvent } from "./IRoomEvent" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IRoomEvent } from "./IRoomEvent"; -export interface ISendToDeviceFromWidgetRequestData - extends IWidgetApiRequestData { - type: string - encrypted: boolean - messages: { [userId: string]: { [deviceId: string]: object } } +export interface ISendToDeviceFromWidgetRequestData extends IWidgetApiRequestData { + type: string; + encrypted: boolean; + messages: { [userId: string]: { [deviceId: string]: object } }; } -export interface ISendToDeviceFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendToDevice - data: ISendToDeviceFromWidgetRequestData +export interface ISendToDeviceFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.SendToDevice; + data: ISendToDeviceFromWidgetRequestData; } -export interface ISendToDeviceFromWidgetResponseData - extends IWidgetApiResponseData { +export interface ISendToDeviceFromWidgetResponseData extends IWidgetApiResponseData { // nothing } -export interface ISendToDeviceFromWidgetActionResponse - extends ISendToDeviceFromWidgetActionRequest { - response: ISendToDeviceFromWidgetResponseData +export interface ISendToDeviceFromWidgetActionResponse extends ISendToDeviceFromWidgetActionRequest { + response: ISendToDeviceFromWidgetResponseData; } -export interface ISendToDeviceToWidgetRequestData - extends IWidgetApiRequestData, - IRoomEvent { - encrypted: boolean +export interface ISendToDeviceToWidgetRequestData extends IWidgetApiRequestData, IRoomEvent { + encrypted: boolean; } export interface ISendToDeviceToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.SendToDevice - data: ISendToDeviceToWidgetRequestData + action: WidgetApiToWidgetAction.SendToDevice; + data: ISendToDeviceToWidgetRequestData; } -export interface ISendToDeviceToWidgetResponseData - extends IWidgetApiResponseData { +export interface ISendToDeviceToWidgetResponseData extends IWidgetApiResponseData { // nothing } -export interface ISendToDeviceToWidgetActionResponse - extends ISendToDeviceToWidgetActionRequest { - response: ISendToDeviceToWidgetResponseData +export interface ISendToDeviceToWidgetActionResponse extends ISendToDeviceToWidgetActionRequest { + response: ISendToDeviceToWidgetResponseData; } diff --git a/src/interfaces/SetModalButtonEnabledAction.ts b/src/interfaces/SetModalButtonEnabledAction.ts index c0eff40..5702e8c 100644 --- a/src/interfaces/SetModalButtonEnabledAction.ts +++ b/src/interfaces/SetModalButtonEnabledAction.ts @@ -14,23 +14,21 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" -import { ModalButtonID } from "./ModalWidgetActions" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; +import { ModalButtonID } from "./ModalWidgetActions"; -export interface ISetModalButtonEnabledActionRequestData - extends IWidgetApiRequestData { - enabled: boolean - button: ModalButtonID +export interface ISetModalButtonEnabledActionRequestData extends IWidgetApiRequestData { + enabled: boolean; + button: ModalButtonID; } export interface ISetModalButtonEnabledActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SetModalButtonEnabled - data: ISetModalButtonEnabledActionRequestData + action: WidgetApiFromWidgetAction.SetModalButtonEnabled; + data: ISetModalButtonEnabledActionRequestData; } -export interface ISetModalButtonEnabledActionResponse - extends ISetModalButtonEnabledActionRequest { - response: IWidgetApiAcknowledgeResponseData +export interface ISetModalButtonEnabledActionResponse extends ISetModalButtonEnabledActionRequest { + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/StickerAction.ts b/src/interfaces/StickerAction.ts index 0db5bff..c7293e3 100644 --- a/src/interfaces/StickerAction.ts +++ b/src/interfaces/StickerAction.ts @@ -14,36 +14,36 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IStickerActionRequestData extends IWidgetApiRequestData { - name: string - description?: string + name: string; + description?: string; content: { - url: string + url: string; info?: { - h?: number - w?: number - mimetype?: string - size?: number + h?: number; + w?: number; + mimetype?: string; + size?: number; thumbnail_info?: { // eslint-disable-line camelcase - h?: number - w?: number - mimetype?: string - size?: number - } - } - } + h?: number; + w?: number; + mimetype?: string; + size?: number; + }; + }; + }; } export interface IStickerActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.SendSticker - data: IStickerActionRequestData + action: WidgetApiFromWidgetAction.SendSticker; + data: IStickerActionRequestData; } export interface IStickerActionResponse extends IStickerActionRequest { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/StickyAction.ts b/src/interfaces/StickyAction.ts index 31d4085..7d49f02 100644 --- a/src/interfaces/StickyAction.ts +++ b/src/interfaces/StickyAction.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface IStickyActionRequestData extends IWidgetApiRequestData { - value: boolean + value: boolean; } export interface IStickyActionRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen - data: IStickyActionRequestData + action: WidgetApiFromWidgetAction.UpdateAlwaysOnScreen; + data: IStickyActionRequestData; } export interface IStickyActionResponseData extends IWidgetApiResponseData { - success: boolean + success: boolean; } export interface IStickyActionResponse extends IStickyActionRequest { - response: IStickyActionResponseData + response: IStickyActionResponseData; } diff --git a/src/interfaces/SupportedVersionsAction.ts b/src/interfaces/SupportedVersionsAction.ts index 7e8ecc1..8486ebc 100644 --- a/src/interfaces/SupportedVersionsAction.ts +++ b/src/interfaces/SupportedVersionsAction.ts @@ -14,30 +14,20 @@ * limitations under the License. */ -import { - IWidgetApiRequest, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { ApiVersion } from "./ApiVersion" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { ApiVersion } from "./ApiVersion"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export interface ISupportedVersionsActionRequest extends IWidgetApiRequest { - action: - | WidgetApiFromWidgetAction.SupportedApiVersions - | WidgetApiToWidgetAction.SupportedApiVersions - data: IWidgetApiRequestEmptyData + action: WidgetApiFromWidgetAction.SupportedApiVersions | WidgetApiToWidgetAction.SupportedApiVersions; + data: IWidgetApiRequestEmptyData; } -export interface ISupportedVersionsActionResponseData - extends IWidgetApiResponseData { - supported_versions: ApiVersion[] // eslint-disable-line camelcase +export interface ISupportedVersionsActionResponseData extends IWidgetApiResponseData { + supported_versions: ApiVersion[]; // eslint-disable-line camelcase } -export interface ISupportedVersionsActionResponse - extends ISupportedVersionsActionRequest { - response: ISupportedVersionsActionResponseData +export interface ISupportedVersionsActionResponse extends ISupportedVersionsActionRequest { + response: ISupportedVersionsActionResponseData; } diff --git a/src/interfaces/ThemeChangeAction.ts b/src/interfaces/ThemeChangeAction.ts index e781d76..292f58e 100644 --- a/src/interfaces/ThemeChangeAction.ts +++ b/src/interfaces/ThemeChangeAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IThemeChangeActionRequestData extends IWidgetApiRequestData { // The format of a theme is deliberately unstandardized } export interface IThemeChangeActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.ThemeChange - data: IThemeChangeActionRequestData + action: WidgetApiToWidgetAction.ThemeChange; + data: IThemeChangeActionRequestData; } export interface IThemeChangeActionResponse extends IThemeChangeActionRequest { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/TurnServerActions.ts b/src/interfaces/TurnServerActions.ts index c89489a..36f664a 100644 --- a/src/interfaces/TurnServerActions.ts +++ b/src/interfaces/TurnServerActions.ts @@ -14,53 +14,41 @@ * limitations under the License. */ -import { - IWidgetApiRequest, - IWidgetApiRequestData, - IWidgetApiRequestEmptyData, -} from "./IWidgetApiRequest" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "./WidgetApiAction" -import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData, IWidgetApiRequestEmptyData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; export interface ITurnServer { - uris: string[] - username: string - password: string + uris: string[]; + username: string; + password: string; } export interface IWatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.WatchTurnServers - data: IWidgetApiRequestEmptyData + action: WidgetApiFromWidgetAction.WatchTurnServers; + data: IWidgetApiRequestEmptyData; } export interface IWatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } export interface IUnwatchTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.UnwatchTurnServers - data: IWidgetApiRequestEmptyData + action: WidgetApiFromWidgetAction.UnwatchTurnServers; + data: IWidgetApiRequestEmptyData; } export interface IUnwatchTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } -export interface IUpdateTurnServersRequestData - extends IWidgetApiRequestData, - ITurnServer {} +export interface IUpdateTurnServersRequestData extends IWidgetApiRequestData, ITurnServer {} export interface IUpdateTurnServersRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateTurnServers - data: IUpdateTurnServersRequestData + action: WidgetApiToWidgetAction.UpdateTurnServers; + data: IUpdateTurnServersRequestData; } export interface IUpdateTurnServersResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/UpdateDelayedEventAction.ts b/src/interfaces/UpdateDelayedEventAction.ts index 80f770e..9ba0179 100644 --- a/src/interfaces/UpdateDelayedEventAction.ts +++ b/src/interfaces/UpdateDelayedEventAction.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; export enum UpdateDelayedEventAction { Cancel = "cancel", @@ -24,24 +24,20 @@ export enum UpdateDelayedEventAction { Send = "send", } -export interface IUpdateDelayedEventFromWidgetRequestData - extends IWidgetApiRequestData { - delay_id: string // eslint-disable-line camelcase - action: UpdateDelayedEventAction +export interface IUpdateDelayedEventFromWidgetRequestData extends IWidgetApiRequestData { + delay_id: string; // eslint-disable-line camelcase + action: UpdateDelayedEventAction; } -export interface IUpdateDelayedEventFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent - data: IUpdateDelayedEventFromWidgetRequestData +export interface IUpdateDelayedEventFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4157UpdateDelayedEvent; + data: IUpdateDelayedEventFromWidgetRequestData; } -export interface IUpdateDelayedEventFromWidgetResponseData - extends IWidgetApiResponseData { +export interface IUpdateDelayedEventFromWidgetResponseData extends IWidgetApiResponseData { // nothing } -export interface IUpdateDelayedEventFromWidgetActionResponse - extends IUpdateDelayedEventFromWidgetActionRequest { - response: IUpdateDelayedEventFromWidgetResponseData +export interface IUpdateDelayedEventFromWidgetActionResponse extends IUpdateDelayedEventFromWidgetActionRequest { + response: IUpdateDelayedEventFromWidgetResponseData; } diff --git a/src/interfaces/UpdateStateAction.ts b/src/interfaces/UpdateStateAction.ts index 798c0cb..c497caf 100644 --- a/src/interfaces/UpdateStateAction.ts +++ b/src/interfaces/UpdateStateAction.ts @@ -14,26 +14,24 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { IRoomEvent } from "./IRoomEvent" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { IRoomEvent } from "./IRoomEvent"; export interface IUpdateStateToWidgetRequestData extends IWidgetApiRequestData { - state: IRoomEvent[] + state: IRoomEvent[]; } export interface IUpdateStateToWidgetActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateState - data: IUpdateStateToWidgetRequestData + action: WidgetApiToWidgetAction.UpdateState; + data: IUpdateStateToWidgetRequestData; } -export interface IUpdateStateToWidgetResponseData - extends IWidgetApiResponseData { +export interface IUpdateStateToWidgetResponseData extends IWidgetApiResponseData { // nothing } -export interface IUpdateStateToWidgetActionResponse - extends IUpdateStateToWidgetActionRequest { - response: IUpdateStateToWidgetResponseData +export interface IUpdateStateToWidgetActionResponse extends IUpdateStateToWidgetActionRequest { + response: IUpdateStateToWidgetResponseData; } diff --git a/src/interfaces/UploadFileAction.ts b/src/interfaces/UploadFileAction.ts index 7bd2cf8..9d120b6 100644 --- a/src/interfaces/UploadFileAction.ts +++ b/src/interfaces/UploadFileAction.ts @@ -14,27 +14,23 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IUploadFileActionFromWidgetRequestData - extends IWidgetApiRequestData { - file: XMLHttpRequestBodyInit +export interface IUploadFileActionFromWidgetRequestData extends IWidgetApiRequestData { + file: XMLHttpRequestBodyInit; } -export interface IUploadFileActionFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC4039UploadFileAction - data: IUploadFileActionFromWidgetRequestData +export interface IUploadFileActionFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC4039UploadFileAction; + data: IUploadFileActionFromWidgetRequestData; } -export interface IUploadFileActionFromWidgetResponseData - extends IWidgetApiResponseData { - content_uri: string // eslint-disable-line camelcase +export interface IUploadFileActionFromWidgetResponseData extends IWidgetApiResponseData { + content_uri: string; // eslint-disable-line camelcase } -export interface IUploadFileActionFromWidgetActionResponse - extends IUploadFileActionFromWidgetActionRequest { - response: IUploadFileActionFromWidgetResponseData +export interface IUploadFileActionFromWidgetActionResponse extends IUploadFileActionFromWidgetActionRequest { + response: IUploadFileActionFromWidgetResponseData; } diff --git a/src/interfaces/UserDirectorySearchAction.ts b/src/interfaces/UserDirectorySearchAction.ts index a46a7b8..fb900cc 100644 --- a/src/interfaces/UserDirectorySearchAction.ts +++ b/src/interfaces/UserDirectorySearchAction.ts @@ -14,33 +14,29 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { IWidgetApiResponseData } from "./IWidgetApiResponse" -import { WidgetApiFromWidgetAction } from "./WidgetApiAction" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { IWidgetApiResponseData } from "./IWidgetApiResponse"; +import { WidgetApiFromWidgetAction } from "./WidgetApiAction"; -export interface IUserDirectorySearchFromWidgetRequestData - extends IWidgetApiRequestData { - search_term: string // eslint-disable-line camelcase - limit?: number +export interface IUserDirectorySearchFromWidgetRequestData extends IWidgetApiRequestData { + search_term: string; // eslint-disable-line camelcase + limit?: number; } -export interface IUserDirectorySearchFromWidgetActionRequest - extends IWidgetApiRequest { - action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch - data: IUserDirectorySearchFromWidgetRequestData +export interface IUserDirectorySearchFromWidgetActionRequest extends IWidgetApiRequest { + action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch; + data: IUserDirectorySearchFromWidgetRequestData; } -export interface IUserDirectorySearchFromWidgetResponseData - extends IWidgetApiResponseData { - limited: boolean +export interface IUserDirectorySearchFromWidgetResponseData extends IWidgetApiResponseData { + limited: boolean; results: Array<{ - user_id: string // eslint-disable-line camelcase - display_name?: string // eslint-disable-line camelcase - avatar_url?: string // eslint-disable-line camelcase - }> + user_id: string; // eslint-disable-line camelcase + display_name?: string; // eslint-disable-line camelcase + avatar_url?: string; // eslint-disable-line camelcase + }>; } -export interface IUserDirectorySearchFromWidgetActionResponse - extends IUserDirectorySearchFromWidgetActionRequest { - response: IUserDirectorySearchFromWidgetResponseData +export interface IUserDirectorySearchFromWidgetActionResponse extends IUserDirectorySearchFromWidgetActionRequest { + response: IUserDirectorySearchFromWidgetResponseData; } diff --git a/src/interfaces/VisibilityAction.ts b/src/interfaces/VisibilityAction.ts index f3f9479..55aa53f 100644 --- a/src/interfaces/VisibilityAction.ts +++ b/src/interfaces/VisibilityAction.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse" +import { IWidgetApiRequest, IWidgetApiRequestData } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData } from "./IWidgetApiResponse"; export interface IVisibilityActionRequestData extends IWidgetApiRequestData { - visible: boolean + visible: boolean; } export interface IVisibilityActionRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.UpdateVisibility - data: IVisibilityActionRequestData + action: WidgetApiToWidgetAction.UpdateVisibility; + data: IVisibilityActionRequestData; } export interface IVisibilityActionResponse extends IVisibilityActionRequest { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/WidgetApiAction.ts b/src/interfaces/WidgetApiAction.ts index 174059e..2f0bcf5 100644 --- a/src/interfaces/WidgetApiAction.ts +++ b/src/interfaces/WidgetApiAction.ts @@ -94,7 +94,4 @@ export enum WidgetApiFromWidgetAction { MSC4157UpdateDelayedEvent = "org.matrix.msc4157.update_delayed_event", } -export type WidgetApiAction = - | WidgetApiToWidgetAction - | WidgetApiFromWidgetAction - | string +export type WidgetApiAction = WidgetApiToWidgetAction | WidgetApiFromWidgetAction | string; diff --git a/src/interfaces/WidgetApiDirection.ts b/src/interfaces/WidgetApiDirection.ts index a4f68b7..e11e144 100644 --- a/src/interfaces/WidgetApiDirection.ts +++ b/src/interfaces/WidgetApiDirection.ts @@ -21,10 +21,10 @@ export enum WidgetApiDirection { export function invertedDirection(dir: WidgetApiDirection): WidgetApiDirection { if (dir === WidgetApiDirection.ToWidget) { - return WidgetApiDirection.FromWidget + return WidgetApiDirection.FromWidget; } else if (dir === WidgetApiDirection.FromWidget) { - return WidgetApiDirection.ToWidget + return WidgetApiDirection.ToWidget; } else { - throw new Error("Invalid direction") + throw new Error("Invalid direction"); } } diff --git a/src/interfaces/WidgetConfigAction.ts b/src/interfaces/WidgetConfigAction.ts index 59f5d7c..b10314c 100644 --- a/src/interfaces/WidgetConfigAction.ts +++ b/src/interfaces/WidgetConfigAction.ts @@ -14,19 +14,16 @@ * limitations under the License. */ -import { IWidgetApiRequest } from "./IWidgetApiRequest" -import { WidgetApiToWidgetAction } from "./WidgetApiAction" -import { - IWidgetApiAcknowledgeResponseData, - IWidgetApiResponse, -} from "./IWidgetApiResponse" -import { IModalWidgetOpenRequestData } from "./ModalWidgetActions" +import { IWidgetApiRequest } from "./IWidgetApiRequest"; +import { WidgetApiToWidgetAction } from "./WidgetApiAction"; +import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponse } from "./IWidgetApiResponse"; +import { IModalWidgetOpenRequestData } from "./ModalWidgetActions"; export interface IWidgetConfigRequest extends IWidgetApiRequest { - action: WidgetApiToWidgetAction.WidgetConfig - data: IModalWidgetOpenRequestData + action: WidgetApiToWidgetAction.WidgetConfig; + data: IModalWidgetOpenRequestData; } export interface IWidgetConfigResponse extends IWidgetApiResponse { - response: IWidgetApiAcknowledgeResponseData + response: IWidgetApiAcknowledgeResponseData; } diff --git a/src/interfaces/WidgetType.ts b/src/interfaces/WidgetType.ts index 12ce681..d6b3e33 100644 --- a/src/interfaces/WidgetType.ts +++ b/src/interfaces/WidgetType.ts @@ -20,4 +20,4 @@ export enum MatrixWidgetType { Stickerpicker = "m.stickerpicker", } -export type WidgetType = MatrixWidgetType | string +export type WidgetType = MatrixWidgetType | string; diff --git a/src/models/Widget.ts b/src/models/Widget.ts index cc4dcf3..0b66452 100644 --- a/src/models/Widget.ts +++ b/src/models/Widget.ts @@ -14,70 +14,70 @@ * limitations under the License. */ -import { IWidget, IWidgetData, WidgetType } from ".." -import { assertPresent } from "./validation/utils" -import { ITemplateParams, runTemplate } from ".." +import { IWidget, IWidgetData, WidgetType } from ".."; +import { assertPresent } from "./validation/utils"; +import { ITemplateParams, runTemplate } from ".."; /** * Represents the barest form of widget. */ export class Widget { public constructor(private definition: IWidget) { - if (!this.definition) throw new Error("Definition is required") + if (!this.definition) throw new Error("Definition is required"); - assertPresent(definition, "id") - assertPresent(definition, "creatorUserId") - assertPresent(definition, "type") - assertPresent(definition, "url") + assertPresent(definition, "id"); + assertPresent(definition, "creatorUserId"); + assertPresent(definition, "type"); + assertPresent(definition, "url"); } /** * The user ID who created the widget. */ public get creatorUserId(): string { - return this.definition.creatorUserId + return this.definition.creatorUserId; } /** * The type of widget. */ public get type(): WidgetType { - return this.definition.type + return this.definition.type; } /** * The ID of the widget. */ public get id(): string { - return this.definition.id + return this.definition.id; } /** * The name of the widget, or null if not set. */ public get name(): string | null { - return this.definition.name || null + return this.definition.name || null; } /** * The title for the widget, or null if not set. */ public get title(): string | null { - return this.rawData.title || null + return this.rawData.title || null; } /** * The templated URL for the widget. */ public get templateUrl(): string { - return this.definition.url + return this.definition.url; } /** * The origin for this widget. */ public get origin(): string { - return new URL(this.templateUrl).origin + return new URL(this.templateUrl).origin; } /** @@ -85,9 +85,9 @@ export class Widget { * to true. */ public get waitForIframeLoad(): boolean { - if (this.definition.waitForIframeLoad === false) return false - if (this.definition.waitForIframeLoad === true) return true - return true // default true + if (this.definition.waitForIframeLoad === false) return false; + if (this.definition.waitForIframeLoad === true) return true; + return true; // default true } /** @@ -95,7 +95,7 @@ export class Widget { * may be empty. */ public get rawData(): IWidgetData { - return this.definition.data || {} + return this.definition.data || {}; } /** @@ -104,6 +104,6 @@ export class Widget { * @returns {string} A templated URL. */ public getCompleteUrl(params: ITemplateParams): string { - return runTemplate(this.templateUrl, this.definition, params) + return runTemplate(this.templateUrl, this.definition, params); } } diff --git a/src/models/WidgetEventCapability.ts b/src/models/WidgetEventCapability.ts index c9bb9a4..1190606 100644 --- a/src/models/WidgetEventCapability.ts +++ b/src/models/WidgetEventCapability.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Capability } from ".." +import { Capability } from ".."; export enum EventKind { Event = "event", @@ -37,63 +37,49 @@ export class WidgetEventCapability { public readonly raw: string, ) {} - public matchesAsStateEvent( - direction: EventDirection, - eventType: string, - stateKey: string | null, - ): boolean { - if (this.kind !== EventKind.State) return false // not a state event - if (this.direction !== direction) return false // direction mismatch - if (this.eventType !== eventType) return false // event type mismatch - if (this.keyStr === null) return true // all state keys are allowed - if (this.keyStr === stateKey) return true // this state key is allowed + public matchesAsStateEvent(direction: EventDirection, eventType: string, stateKey: string | null): boolean { + if (this.kind !== EventKind.State) return false; // not a state event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch + if (this.keyStr === null) return true; // all state keys are allowed + if (this.keyStr === stateKey) return true; // this state key is allowed // Default not allowed - return false + return false; } - public matchesAsToDeviceEvent( - direction: EventDirection, - eventType: string, - ): boolean { - if (this.kind !== EventKind.ToDevice) return false // not a to-device event - if (this.direction !== direction) return false // direction mismatch - if (this.eventType !== eventType) return false // event type mismatch + public matchesAsToDeviceEvent(direction: EventDirection, eventType: string): boolean { + if (this.kind !== EventKind.ToDevice) return false; // not a to-device event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch // Checks passed, the event is allowed - return true + return true; } - public matchesAsRoomEvent( - direction: EventDirection, - eventType: string, - msgtype: string | null = null, - ): boolean { - if (this.kind !== EventKind.Event) return false // not a room event - if (this.direction !== direction) return false // direction mismatch - if (this.eventType !== eventType) return false // event type mismatch + public matchesAsRoomEvent(direction: EventDirection, eventType: string, msgtype: string | null = null): boolean { + if (this.kind !== EventKind.Event) return false; // not a room event + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch if (this.eventType === "m.room.message") { - if (this.keyStr === null) return true // all message types are allowed - if (this.keyStr === msgtype) return true // this message type is allowed + if (this.keyStr === null) return true; // all message types are allowed + if (this.keyStr === msgtype) return true; // this message type is allowed } else { - return true // already passed the check for if the event is allowed + return true; // already passed the check for if the event is allowed } // Default not allowed - return false + return false; } - public matchesAsRoomAccountData( - direction: EventDirection, - eventType: string, - ): boolean { - if (this.kind !== EventKind.RoomAccount) return false // not room account data - if (this.direction !== direction) return false // direction mismatch - if (this.eventType !== eventType) return false // event type mismatch + public matchesAsRoomAccountData(direction: EventDirection, eventType: string): boolean { + if (this.kind !== EventKind.RoomAccount) return false; // not room account data + if (this.direction !== direction) return false; // direction mismatch + if (this.eventType !== eventType) return false; // event type mismatch // Checks passed, the event is allowed - return true + return true; } public static forStateEvent( @@ -103,59 +89,46 @@ export class WidgetEventCapability { ): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/22 - eventType = eventType.replace(/#/g, "\\#") - stateKey = - stateKey !== null && stateKey !== undefined ? `#${stateKey}` : "" - const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}` + eventType = eventType.replace(/#/g, "\\#"); + stateKey = stateKey !== null && stateKey !== undefined ? `#${stateKey}` : ""; + const str = `org.matrix.msc2762.${direction}.state_event:${eventType}${stateKey}`; // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0] + return WidgetEventCapability.findEventCapabilities([str])[0]; } - public static forToDeviceEvent( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { + public static forToDeviceEvent(direction: EventDirection, eventType: string): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/56 - const str = `org.matrix.msc3819.${direction}.to_device:${eventType}` + const str = `org.matrix.msc3819.${direction}.to_device:${eventType}`; // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0] + return WidgetEventCapability.findEventCapabilities([str])[0]; } - public static forRoomEvent( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { + public static forRoomEvent(direction: EventDirection, eventType: string): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/22 - const str = `org.matrix.msc2762.${direction}.event:${eventType}` + const str = `org.matrix.msc2762.${direction}.event:${eventType}`; // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0] + return WidgetEventCapability.findEventCapabilities([str])[0]; } - public static forRoomMessageEvent( - direction: EventDirection, - msgtype?: string, - ): WidgetEventCapability { + public static forRoomMessageEvent(direction: EventDirection, msgtype?: string): WidgetEventCapability { // TODO: Enable support for m.* namespace once the MSC lands. // https://github.com/matrix-org/matrix-widget-api/issues/22 - msgtype = msgtype === null || msgtype === undefined ? "" : msgtype - const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}` + msgtype = msgtype === null || msgtype === undefined ? "" : msgtype; + const str = `org.matrix.msc2762.${direction}.event:m.room.message#${msgtype}`; // cheat by sending it through the processor - return WidgetEventCapability.findEventCapabilities([str])[0] + return WidgetEventCapability.findEventCapabilities([str])[0]; } - public static forRoomAccountData( - direction: EventDirection, - eventType: string, - ): WidgetEventCapability { - const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}` + public static forRoomAccountData(direction: EventDirection, eventType: string): WidgetEventCapability { + const str = `com.beeper.capabilities.${direction}.room_account_data:${eventType}`; - return WidgetEventCapability.findEventCapabilities([str])[0] + return WidgetEventCapability.findEventCapabilities([str])[0]; } /** @@ -163,86 +136,55 @@ export class WidgetEventCapability { * @param {Iterable} capabilities The capabilities requested/to parse. * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. */ - public static findEventCapabilities( - capabilities: Iterable, - ): WidgetEventCapability[] { - const parsed: WidgetEventCapability[] = [] + public static findEventCapabilities(capabilities: Iterable): WidgetEventCapability[] { + const parsed: WidgetEventCapability[] = []; for (const cap of capabilities) { - let direction: EventDirection | null = null - let eventSegment: string | undefined - let kind: EventKind | null = null + let direction: EventDirection | null = null; + let eventSegment: string | undefined; + let kind: EventKind | null = null; // TODO: Enable support for m.* namespace once the MSCs land. // https://github.com/matrix-org/matrix-widget-api/issues/22 // https://github.com/matrix-org/matrix-widget-api/issues/56 if (cap.startsWith("org.matrix.msc2762.send.event:")) { - direction = EventDirection.Send - kind = EventKind.Event - eventSegment = cap.substring( - "org.matrix.msc2762.send.event:".length, - ) + direction = EventDirection.Send; + kind = EventKind.Event; + eventSegment = cap.substring("org.matrix.msc2762.send.event:".length); } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { - direction = EventDirection.Send - kind = EventKind.State - eventSegment = cap.substring( - "org.matrix.msc2762.send.state_event:".length, - ) + direction = EventDirection.Send; + kind = EventKind.State; + eventSegment = cap.substring("org.matrix.msc2762.send.state_event:".length); } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { - direction = EventDirection.Send - kind = EventKind.ToDevice - eventSegment = cap.substring( - "org.matrix.msc3819.send.to_device:".length, - ) + direction = EventDirection.Send; + kind = EventKind.ToDevice; + eventSegment = cap.substring("org.matrix.msc3819.send.to_device:".length); } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { - direction = EventDirection.Receive - kind = EventKind.Event - eventSegment = cap.substring( - "org.matrix.msc2762.receive.event:".length, - ) - } else if ( - cap.startsWith("org.matrix.msc2762.receive.state_event:") - ) { - direction = EventDirection.Receive - kind = EventKind.State - eventSegment = cap.substring( - "org.matrix.msc2762.receive.state_event:".length, - ) - } else if ( - cap.startsWith("org.matrix.msc3819.receive.to_device:") - ) { - direction = EventDirection.Receive - kind = EventKind.ToDevice - eventSegment = cap.substring( - "org.matrix.msc3819.receive.to_device:".length, - ) - } else if ( - cap.startsWith( - "com.beeper.capabilities.receive.room_account_data:", - ) - ) { - direction = EventDirection.Receive - kind = EventKind.RoomAccount - eventSegment = cap.substring( - "com.beeper.capabilities.receive.room_account_data:".length, - ) + direction = EventDirection.Receive; + kind = EventKind.Event; + eventSegment = cap.substring("org.matrix.msc2762.receive.event:".length); + } else if (cap.startsWith("org.matrix.msc2762.receive.state_event:")) { + direction = EventDirection.Receive; + kind = EventKind.State; + eventSegment = cap.substring("org.matrix.msc2762.receive.state_event:".length); + } else if (cap.startsWith("org.matrix.msc3819.receive.to_device:")) { + direction = EventDirection.Receive; + kind = EventKind.ToDevice; + eventSegment = cap.substring("org.matrix.msc3819.receive.to_device:".length); + } else if (cap.startsWith("com.beeper.capabilities.receive.room_account_data:")) { + direction = EventDirection.Receive; + kind = EventKind.RoomAccount; + eventSegment = cap.substring("com.beeper.capabilities.receive.room_account_data:".length); } - if ( - direction === null || - kind === null || - eventSegment === undefined - ) - continue + if (direction === null || kind === null || eventSegment === undefined) continue; // The capability uses `#` as a separator between event type and state key/msgtype, // so we split on that. However, a # is also valid in either one of those so we // join accordingly. // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". - const expectingKeyStr = - eventSegment.startsWith("m.room.message#") || - kind === EventKind.State - let keyStr: string | null = null + const expectingKeyStr = eventSegment.startsWith("m.room.message#") || kind === EventKind.State; + let keyStr: string | null = null; if (eventSegment.includes("#") && expectingKeyStr) { // Dev note: regex is difficult to write, so instead the rules are manually written // out. This is probably just as understandable as a boring regex though, so win-win? @@ -259,34 +201,24 @@ export class WidgetEventCapability { // m.room.message\\###test m.room.message\# #test // First step: explode the string - const parts = eventSegment.split("#") + const parts = eventSegment.split("#"); // To form the eventSegment, we'll keep finding parts of the exploded string until // there's one that doesn't end with the escape character (\). We'll then join those // segments together with the exploding character. We have to remember to consume the // escape character as well. - const idx = parts.findIndex((p) => !p.endsWith("\\")) + const idx = parts.findIndex((p) => !p.endsWith("\\")); eventSegment = parts .slice(0, idx + 1) - .map((p) => - p.endsWith("\\") ? p.substring(0, p.length - 1) : p, - ) - .join("#") + .map((p) => (p.endsWith("\\") ? p.substring(0, p.length - 1) : p)) + .join("#"); // The keyStr is whatever is left over. - keyStr = parts.slice(idx + 1).join("#") + keyStr = parts.slice(idx + 1).join("#"); } - parsed.push( - new WidgetEventCapability( - direction, - eventSegment, - kind, - keyStr, - cap, - ), - ) + parsed.push(new WidgetEventCapability(direction, eventSegment, kind, keyStr, cap)); } - return parsed + return parsed; } } diff --git a/src/models/WidgetParser.ts b/src/models/WidgetParser.ts index 90e5058..07ced72 100644 --- a/src/models/WidgetParser.ts +++ b/src/models/WidgetParser.ts @@ -14,30 +14,30 @@ * limitations under the License. */ -import { Widget } from "./Widget" -import { IWidget } from ".." -import { isValidUrl } from "./validation/url" +import { Widget } from "./Widget"; +import { IWidget } from ".."; +import { isValidUrl } from "./validation/url"; export interface IStateEvent { - event_id: string // eslint-disable-line camelcase - room_id: string // eslint-disable-line camelcase - type: string - sender: string - origin_server_ts: number // eslint-disable-line camelcase - unsigned?: unknown - content: unknown - state_key: string // eslint-disable-line camelcase + event_id: string; // eslint-disable-line camelcase + room_id: string; // eslint-disable-line camelcase + type: string; + sender: string; + origin_server_ts: number; // eslint-disable-line camelcase + unsigned?: unknown; + content: unknown; + state_key: string; // eslint-disable-line camelcase } export interface IAccountDataWidgets { [widgetId: string]: { - type: "m.widget" + type: "m.widget"; // the state_key is also the widget's ID - state_key: string // eslint-disable-line camelcase - sender: string // current user's ID - content: IWidget - id?: string // off-spec, but possible - } + state_key: string; // eslint-disable-line camelcase + sender: string; // current user's ID + content: IWidget; + id?: string; // off-spec, but possible + }; } export class WidgetParser { @@ -52,21 +52,17 @@ export class WidgetParser { * @returns {Widget[]} The widgets in account data, or an empty array. */ public static parseAccountData(content: IAccountDataWidgets): Widget[] { - if (!content) return [] + if (!content) return []; - const result: Widget[] = [] + const result: Widget[] = []; for (const widgetId of Object.keys(content)) { - const roughWidget = content[widgetId] - if (!roughWidget) continue - if ( - roughWidget.type !== "m.widget" && - roughWidget.type !== "im.vector.modular.widgets" - ) - continue - if (!roughWidget.sender) continue - - const probableWidgetId = roughWidget.state_key || roughWidget.id - if (probableWidgetId !== widgetId) continue + const roughWidget = content[widgetId]; + if (!roughWidget) continue; + if (roughWidget.type !== "m.widget" && roughWidget.type !== "im.vector.modular.widgets") continue; + if (!roughWidget.sender) continue; + + const probableWidgetId = roughWidget.state_key || roughWidget.id; + if (probableWidgetId !== widgetId) continue; const asStateEvent: IStateEvent = { content: roughWidget.content, @@ -76,13 +72,13 @@ export class WidgetParser { event_id: "$example", room_id: "!example", origin_server_ts: 1, - } + }; - const widget = WidgetParser.parseRoomWidget(asStateEvent) - if (widget) result.push(widget) + const widget = WidgetParser.parseRoomWidget(asStateEvent); + if (widget) result.push(widget); } - return result + return result; } /** @@ -91,16 +87,14 @@ export class WidgetParser { * @param {IStateEvent[]} currentState The room state to parse. * @returns {Widget[]} The widgets in the state, or an empty array. */ - public static parseWidgetsFromRoomState( - currentState: IStateEvent[], - ): Widget[] { - if (!currentState) return [] - const result: Widget[] = [] + public static parseWidgetsFromRoomState(currentState: IStateEvent[]): Widget[] { + if (!currentState) return []; + const result: Widget[] = []; for (const state of currentState) { - const widget = WidgetParser.parseRoomWidget(state) - if (widget) result.push(widget) + const widget = WidgetParser.parseRoomWidget(state); + if (widget) result.push(widget); } - return result + return result; } /** @@ -110,14 +104,11 @@ export class WidgetParser { * @returns {Widget|null} The widget, or null if invalid */ public static parseRoomWidget(stateEvent: IStateEvent): Widget | null { - if (!stateEvent) return null + if (!stateEvent) return null; // TODO: [Legacy] Remove legacy support - if ( - stateEvent.type !== "m.widget" && - stateEvent.type !== "im.vector.modular.widgets" - ) { - return null + if (stateEvent.type !== "m.widget" && stateEvent.type !== "im.vector.modular.widgets") { + return null; } // Dev note: Throughout this function we have null safety to ensure that @@ -125,7 +116,7 @@ export class WidgetParser { // is done against the requirements of the interface because not everyone // will have an interface to validate against. - const content = (stateEvent.content as IWidget) || {} + const content = (stateEvent.content as IWidget) || {}; // Form our best approximation of a widget with the information we have const estimatedWidget: IWidget = { @@ -136,21 +127,21 @@ export class WidgetParser { url: content["url"], waitForIframeLoad: content["waitForIframeLoad"], data: content["data"], - } + }; // Finally, process that widget - return WidgetParser.processEstimatedWidget(estimatedWidget) + return WidgetParser.processEstimatedWidget(estimatedWidget); } private static processEstimatedWidget(widget: IWidget): Widget | null { // Validate that the widget has the best chance of passing as a widget if (!widget.id || !widget.creatorUserId || !widget.type) { - return null + return null; } if (!isValidUrl(widget.url)) { - return null + return null; } // TODO: Validate data for known widget types - return new Widget(widget) + return new Widget(widget); } } diff --git a/src/models/validation/url.ts b/src/models/validation/url.ts index 7e2a43e..c56a9c6 100644 --- a/src/models/validation/url.ts +++ b/src/models/validation/url.ts @@ -15,18 +15,18 @@ */ export function isValidUrl(val: string): boolean { - if (!val) return false // easy: not valid if not present + if (!val) return false; // easy: not valid if not present try { - const parsed = new URL(val) + const parsed = new URL(val); if (parsed.protocol !== "http" && parsed.protocol !== "https") { - return false + return false; } - return true + return true; } catch (e) { if (e instanceof TypeError) { - return false + return false; } - throw e + throw e; } } diff --git a/src/models/validation/utils.ts b/src/models/validation/utils.ts index 0410a95..5572c0f 100644 --- a/src/models/validation/utils.ts +++ b/src/models/validation/utils.ts @@ -15,11 +15,8 @@ */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function assertPresent>( - obj: O, - key: keyof O, -): void { +export function assertPresent>(obj: O, key: keyof O): void { if (!obj[key]) { - throw new Error(`${String(key)} is required`) + throw new Error(`${String(key)} is required`); } } diff --git a/src/templating/url-template.ts b/src/templating/url-template.ts index 8fb42cb..b700a9b 100644 --- a/src/templating/url-template.ts +++ b/src/templating/url-template.ts @@ -14,32 +14,28 @@ * limitations under the License. */ -import { IWidget } from ".." +import { IWidget } from ".."; export interface ITemplateParams { - widgetRoomId?: string - currentUserId: string - userDisplayName?: string - userHttpAvatarUrl?: string - clientId?: string - clientTheme?: string - clientLanguage?: string - deviceId?: string - baseUrl?: string + widgetRoomId?: string; + currentUserId: string; + userDisplayName?: string; + userHttpAvatarUrl?: string; + clientId?: string; + clientTheme?: string; + clientLanguage?: string; + deviceId?: string; + baseUrl?: string; } -export function runTemplate( - url: string, - widget: IWidget, - params: ITemplateParams, -): string { +export function runTemplate(url: string, widget: IWidget, params: ITemplateParams): string { // Always apply the supplied params over top of data to ensure the data can't lie about them. const variables = Object.assign({}, widget.data, { - matrix_room_id: params.widgetRoomId || "", - matrix_user_id: params.currentUserId, - matrix_display_name: params.userDisplayName || params.currentUserId, - matrix_avatar_url: params.userHttpAvatarUrl || "", - matrix_widget_id: widget.id, + "matrix_room_id": params.widgetRoomId || "", + "matrix_user_id": params.currentUserId, + "matrix_display_name": params.userDisplayName || params.currentUserId, + "matrix_avatar_url": params.userHttpAvatarUrl || "", + "matrix_widget_id": widget.id, // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) "org.matrix.msc2873.client_id": params.clientId || "", @@ -51,28 +47,25 @@ export function runTemplate( // TODO: Convert to stable (https://github.com/matrix-org/matrix-spec-proposals/pull/4039) "org.matrix.msc4039.matrix_base_url": params.baseUrl || "", - }) - let result = url + }); + let result = url; for (const key of Object.keys(variables)) { // Regex escape from https://stackoverflow.com/a/6969486/7037379 - const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string - const rexp = new RegExp(pattern, "g") + const pattern = `$${key}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + const rexp = new RegExp(pattern, "g"); // This is technically not what we're supposed to do for a couple of reasons: // 1. We are assuming that there won't later be a $key match after we replace a variable. // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). - result = result.replace( - rexp, - encodeURIComponent(toString(variables[key])), - ) + result = result.replace(rexp, encodeURIComponent(toString(variables[key]))); } - return result + return result; } export function toString(a: unknown): string { if (a === null || a === undefined) { - return `${a}` + return `${a}`; } // eslint-disable-next-line @typescript-eslint/no-base-to-string - return String(a) + return String(a); } diff --git a/src/transport/ITransport.ts b/src/transport/ITransport.ts index e28fb63..3446e6a 100644 --- a/src/transport/ITransport.ts +++ b/src/transport/ITransport.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EventEmitter } from "events" +import { EventEmitter } from "events"; import { IWidgetApiAcknowledgeResponseData, @@ -23,7 +23,7 @@ import { IWidgetApiResponse, IWidgetApiResponseData, WidgetApiAction, -} from ".." +} from ".."; /** * A transport for widget requests/responses. All actions @@ -34,41 +34,41 @@ export interface ITransport extends EventEmitter { /** * True if the transport is ready to start sending, false otherwise. */ - readonly ready: boolean + readonly ready: boolean; /** * The widget ID, if known. If not known, null. */ - readonly widgetId: string | null + readonly widgetId: string | null; /** * If true, the transport will refuse requests from origins other than the * widget's current origin. This is intended to be used only by widgets which * need excess security. */ - strictOriginCheck: boolean + strictOriginCheck: boolean; /** * The origin the transport should be replying/sending to. If not known, leave * null. */ - targetOrigin: string | null + targetOrigin: string | null; /** * The number of seconds an outbound request is allowed to take before it * times out. */ - timeoutSeconds: number + timeoutSeconds: number; /** * Starts the transport for listening */ - start(): void + start(): void; /** * Stops the transport. It cannot be re-started. */ - stop(): void + stop(): void; /** * Sends a request to the remote end. @@ -79,13 +79,10 @@ export interface ITransport extends EventEmitter { * @throws {WidgetApiResponseError} if the request failed with error details * that can be communicated to the Widget API. */ - send< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData, - >( + send( action: WidgetApiAction, data: T, - ): Promise + ): Promise; /** * Sends a request to the remote end. This is similar to the send() function @@ -101,15 +98,12 @@ export interface ITransport extends EventEmitter { sendComplete( action: WidgetApiAction, data: T, - ): Promise + ): Promise; /** * Replies to a request. * @param {IWidgetApiRequest} request The request to reply to. * @param {IWidgetApiResponseData} responseData The response data to reply with. */ - reply( - request: IWidgetApiRequest, - responseData: T, - ): void + reply(request: IWidgetApiRequest, responseData: T): void; } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index e453930..d6d502d 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { EventEmitter } from "events" +import { EventEmitter } from "events"; -import { ITransport } from "./ITransport" +import { ITransport } from "./ITransport"; import { invertedDirection, isErrorResponse, @@ -28,33 +28,33 @@ import { WidgetApiAction, WidgetApiDirection, WidgetApiToWidgetAction, -} from ".." +} from ".."; interface IOutboundRequest { - request: IWidgetApiRequest - resolve: (response: IWidgetApiResponse) => void - reject: (err: Error) => void + request: IWidgetApiRequest; + resolve: (response: IWidgetApiResponse) => void; + reject: (err: Error) => void; } /** * Transport for the Widget API over postMessage. */ export class PostmessageTransport extends EventEmitter implements ITransport { - public strictOriginCheck = false - public targetOrigin = "*" - public timeoutSeconds = 10 + public strictOriginCheck = false; + public targetOrigin = "*"; + public timeoutSeconds = 10; - private _ready = false - private _widgetId: string | null = null - private outboundRequests = new Map() - private stopController = new AbortController() + private _ready = false; + private _widgetId: string | null = null; + private outboundRequests = new Map(); + private stopController = new AbortController(); public get ready(): boolean { - return this._ready + return this._ready; } public get widgetId(): string | null { - return this._widgetId || null + return this._widgetId || null; } public constructor( @@ -63,57 +63,49 @@ export class PostmessageTransport extends EventEmitter implements ITransport { private transportWindow: Window, private inboundWindow: Window, ) { - super() - this._widgetId = initialWidgetId + super(); + this._widgetId = initialWidgetId; } private get nextRequestId(): string { - const idBase = `widgetapi-${Date.now()}` - let index = 0 - let id = idBase + const idBase = `widgetapi-${Date.now()}`; + let index = 0; + let id = idBase; while (this.outboundRequests.has(id)) { - id = `${idBase}-${index++}` + id = `${idBase}-${index++}`; } // reserve the ID - this.outboundRequests.set(id, null) + this.outboundRequests.set(id, null); - return id + return id; } - private sendInternal( - message: IWidgetApiRequest | IWidgetApiResponse, - ): void { - console.log( - `[PostmessageTransport] Sending object to ${this.targetOrigin}: `, - message, - ) - this.transportWindow.postMessage(message, this.targetOrigin) + private sendInternal(message: IWidgetApiRequest | IWidgetApiResponse): void { + console.log(`[PostmessageTransport] Sending object to ${this.targetOrigin}: `, message); + this.transportWindow.postMessage(message, this.targetOrigin); } - public reply( - request: IWidgetApiRequest, - responseData: T, - ): void { + public reply(request: IWidgetApiRequest, responseData: T): void { return this.sendInternal({ ...request, response: responseData, - }) + }); } - public send< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponseData, - >(action: WidgetApiAction, data: T): Promise { - return this.sendComplete(action, data).then((r) => r.response) + public send( + action: WidgetApiAction, + data: T, + ): Promise { + return this.sendComplete(action, data).then((r) => r.response); } - public sendComplete< - T extends IWidgetApiRequestData, - R extends IWidgetApiResponse, - >(action: WidgetApiAction, data: T): Promise { + public sendComplete( + action: WidgetApiAction, + data: T, + ): Promise { if (!this.ready || !this.widgetId) { - return Promise.reject(new Error("Not ready or unknown widget ID")) + return Promise.reject(new Error("Not ready or unknown widget ID")); } const request: IWidgetApiRequest = { api: this.sendDirection, @@ -121,99 +113,95 @@ export class PostmessageTransport extends EventEmitter implements ITransport { requestId: this.nextRequestId, action: action, data: data, - } + }; if (action === WidgetApiToWidgetAction.UpdateVisibility) { - request["visible"] = data["visible"] + request["visible"] = data["visible"]; } return new Promise((prResolve, prReject) => { const resolve = (response: IWidgetApiResponse): void => { - cleanUp() - prResolve(response) - } + cleanUp(); + prResolve(response); + }; const reject = (err: Error): void => { - cleanUp() - prReject(err) - } + cleanUp(); + prReject(err); + }; - const timerId = setTimeout( - () => reject(new Error("Request timed out")), - (this.timeoutSeconds || 1) * 1000, - ) + const timerId = setTimeout(() => reject(new Error("Request timed out")), (this.timeoutSeconds || 1) * 1000); - const onStop = (): void => reject(new Error("Transport stopped")) - this.stopController.signal.addEventListener("abort", onStop) + const onStop = (): void => reject(new Error("Transport stopped")); + this.stopController.signal.addEventListener("abort", onStop); const cleanUp = (): void => { - this.outboundRequests.delete(request.requestId) - clearTimeout(timerId) - this.stopController.signal.removeEventListener("abort", onStop) - } + this.outboundRequests.delete(request.requestId); + clearTimeout(timerId); + this.stopController.signal.removeEventListener("abort", onStop); + }; this.outboundRequests.set(request.requestId, { request, resolve, reject, - }) - this.sendInternal(request) - }) + }); + this.sendInternal(request); + }); } public start(): void { this.inboundWindow.addEventListener("message", (ev: MessageEvent) => { - this.handleMessage(ev) - }) - this._ready = true + this.handleMessage(ev); + }); + this._ready = true; } public stop(): void { - this._ready = false - this.stopController.abort() + this._ready = false; + this.stopController.abort(); } private handleMessage(ev: MessageEvent): void { - if (this.stopController.signal.aborted) return - if (!ev.data) return // invalid event + if (this.stopController.signal.aborted) return; + if (!ev.data) return; // invalid event - if (this.strictOriginCheck && ev.origin !== window.origin) return // bad origin + if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin // treat the message as a response first, then downgrade to a request - const response = ev.data - if (!response.action || !response.requestId || !response.widgetId) - return // invalid request/response + const response = ev.data; + if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response if (!response.response) { // it's a request - const request = response - if (request.api !== invertedDirection(this.sendDirection)) return // wrong direction - this.handleRequest(request) + const request = response; + if (request.api !== invertedDirection(this.sendDirection)) return; // wrong direction + this.handleRequest(request); } else { // it's a response - if (response.api !== this.sendDirection) return // wrong direction - this.handleResponse(response) + if (response.api !== this.sendDirection) return; // wrong direction + this.handleResponse(response); } } private handleRequest(request: IWidgetApiRequest): void { if (this.widgetId) { - if (this.widgetId !== request.widgetId) return // wrong widget + if (this.widgetId !== request.widgetId) return; // wrong widget } else { - this._widgetId = request.widgetId + this._widgetId = request.widgetId; } - this.emit("message", new CustomEvent("message", { detail: request })) + this.emit("message", new CustomEvent("message", { detail: request })); } private handleResponse(response: IWidgetApiResponse): void { - if (response.widgetId !== this.widgetId) return // wrong widget + if (response.widgetId !== this.widgetId) return; // wrong widget - const req = this.outboundRequests.get(response.requestId) - if (!req) return // response to an unknown request + const req = this.outboundRequests.get(response.requestId); + if (!req) return; // response to an unknown request if (isErrorResponse(response.response)) { - const { message, ...data } = response.response.error - req.reject(new WidgetApiResponseError(message, data)) + const { message, ...data } = response.response.error; + req.reject(new WidgetApiResponseError(message, data)); } else { - req.resolve(response) + req.resolve(response); } } } diff --git a/src/util/SimpleObservable.ts b/src/util/SimpleObservable.ts index c61c510..5108247 100644 --- a/src/util/SimpleObservable.ts +++ b/src/util/SimpleObservable.ts @@ -14,26 +14,26 @@ * limitations under the License. */ -export type ObservableFunction = (val: T) => void +export type ObservableFunction = (val: T) => void; export class SimpleObservable { - private listeners: ObservableFunction[] = [] + private listeners: ObservableFunction[] = []; public constructor(initialFn?: ObservableFunction) { - if (initialFn) this.listeners.push(initialFn) + if (initialFn) this.listeners.push(initialFn); } public onUpdate(fn: ObservableFunction): void { - this.listeners.push(fn) + this.listeners.push(fn); } public update(val: T): void { for (const listener of this.listeners) { - listener(val) + listener(val); } } public close(): void { - this.listeners = [] // reset + this.listeners = []; // reset } } diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index debad49..7a7e94e 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -15,24 +15,21 @@ * limitations under the License. */ -import { waitFor } from "@testing-library/dom" - -import { ClientWidgetApi } from "../src/ClientWidgetApi" -import { WidgetDriver } from "../src/driver/WidgetDriver" -import { UnstableApiVersion } from "../src/interfaces/ApiVersion" -import { Capability } from "../src/interfaces/Capabilities" -import { IRoomEvent } from "../src/interfaces/IRoomEvent" -import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest" -import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction" -import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction" -import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction" -import { - WidgetApiFromWidgetAction, - WidgetApiToWidgetAction, -} from "../src/interfaces/WidgetApiAction" -import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection" -import { Widget } from "../src/models/Widget" -import { PostmessageTransport } from "../src/transport/PostmessageTransport" +import { waitFor } from "@testing-library/dom"; + +import { ClientWidgetApi } from "../src/ClientWidgetApi"; +import { WidgetDriver } from "../src/driver/WidgetDriver"; +import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { Capability } from "../src/interfaces/Capabilities"; +import { IRoomEvent } from "../src/interfaces/IRoomEvent"; +import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; +import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; +import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; +import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; +import { Widget } from "../src/models/Widget"; +import { PostmessageTransport } from "../src/transport/PostmessageTransport"; import { IDownloadFileActionFromWidgetActionRequest, IGetOpenIDActionRequest, @@ -48,15 +45,15 @@ import { SimpleObservable, Symbols, UpdateDelayedEventAction, -} from "../src" -import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction" -import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction" +} from "../src"; +import { IGetMediaConfigActionFromWidgetActionRequest } from "../src/interfaces/GetMediaConfigAction"; +import { IReadRoomAccountDataFromWidgetActionRequest } from "../src/interfaces/ReadRoomAccountDataAction"; -jest.mock("../src/transport/PostmessageTransport") +jest.mock("../src/transport/PostmessageTransport"); afterEach(() => { - jest.resetAllMocks() -}) + jest.resetAllMocks(); +}); function createRoomEvent(event: Partial = {}): IRoomEvent { return { @@ -68,7 +65,7 @@ function createRoomEvent(event: Partial = {}): IRoomEvent { room_id: "!room-id", unsigned: {}, ...event, - } + }; } class CustomMatrixError extends Error { @@ -78,13 +75,11 @@ class CustomMatrixError extends Error { public readonly name: string, public readonly data: Record, ) { - super(message) + super(message); } } -function processCustomMatrixError( - e: unknown, -): IWidgetApiErrorResponseDataDetails | undefined { +function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetails | undefined { return e instanceof CustomMatrixError ? { matrix_api_error: { @@ -98,33 +93,33 @@ function processCustomMatrixError( }, }, } - : undefined + : undefined; } describe("ClientWidgetApi", () => { - let capabilities: Capability[] - let iframe: HTMLIFrameElement - let driver: jest.Mocked - let clientWidgetApi: ClientWidgetApi - let transport: PostmessageTransport - let emitEvent: Parameters["1"] + let capabilities: Capability[]; + let iframe: HTMLIFrameElement; + let driver: jest.Mocked; + let clientWidgetApi: ClientWidgetApi; + let transport: PostmessageTransport; + let emitEvent: Parameters["1"]; async function loadIframe(caps: Capability[] = []): Promise { - capabilities = caps + capabilities = caps; const ready = new Promise((resolve) => { - clientWidgetApi.once("ready", resolve) - }) + clientWidgetApi.once("ready", resolve); + }); - iframe.dispatchEvent(new Event("load")) + iframe.dispatchEvent(new Event("load")); - await ready + await ready; } beforeEach(() => { - capabilities = [] - iframe = document.createElement("iframe") - document.body.appendChild(iframe) + capabilities = []; + iframe = document.createElement("iframe"); + document.body.appendChild(iframe); driver = { navigate: jest.fn(), @@ -144,7 +139,7 @@ describe("ClientWidgetApi", () => { downloadFile: jest.fn(), getKnownRooms: jest.fn(() => []), processError: jest.fn(), - } as Partial as jest.Mocked + } as Partial as jest.Mocked; clientWidgetApi = new ClientWidgetApi( new Widget({ @@ -155,31 +150,29 @@ describe("ClientWidgetApi", () => { }), iframe, driver, - ) - ;[transport] = jest.mocked(PostmessageTransport).mock.instances - emitEvent = jest.mocked(transport.on).mock.calls[0][1] + ); + [transport] = jest.mocked(PostmessageTransport).mock.instances; + emitEvent = jest.mocked(transport.on).mock.calls[0][1]; - jest.mocked(transport.send).mockResolvedValue({}) - jest.mocked(driver.validateCapabilities).mockImplementation( - async () => new Set(capabilities), - ) - }) + jest.mocked(transport.send).mockResolvedValue({}); + jest.mocked(driver.validateCapabilities).mockImplementation(async () => new Set(capabilities)); + }); afterEach(() => { - clientWidgetApi.stop() - iframe.remove() - }) + clientWidgetApi.stop(); + iframe.remove(); + }); it("should initiate capabilities", async () => { - await loadIframe(["m.always_on_screen"]) + await loadIframe(["m.always_on_screen"]); - expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true) - expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false) - }) + expect(clientWidgetApi.hasCapability("m.always_on_screen")).toBe(true); + expect(clientWidgetApi.hasCapability("m.sticker")).toBe(false); + }); describe("navigate action", () => { it("navigates", async () => { - driver.navigate.mockResolvedValue(Promise.resolve()) + driver.navigate.mockResolvedValue(Promise.resolve()); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, @@ -189,18 +182,18 @@ describe("ClientWidgetApi", () => { data: { uri: "https://matrix.to/#/#room:example.net", }, - } + }; - await loadIframe(["org.matrix.msc2931.navigate"]) + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}) - }) + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); - expect(driver.navigate).toHaveBeenCalledWith(event.data.uri) - }) + expect(driver.navigate).toHaveBeenCalledWith(event.data.uri); + }); it("fails to navigate", async () => { const event: INavigateActionRequest = { @@ -211,20 +204,20 @@ describe("ClientWidgetApi", () => { data: { uri: "https://matrix.to/#/#room:example.net", }, - } + }; - await loadIframe([]) // Without the required capability + await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Missing capability" }, - }) - }) + }); + }); - expect(driver.navigate).not.toBeCalled() - }) + expect(driver.navigate).not.toBeCalled(); + }); it("fails to navigate to an unsupported URI", async () => { const event: INavigateActionRequest = { @@ -235,25 +228,23 @@ describe("ClientWidgetApi", () => { data: { uri: "https://example.net", }, - } + }; - await loadIframe(["org.matrix.msc2931.navigate"]) + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid matrix.to URI" }, - }) - }) + }); + }); - expect(driver.navigate).not.toBeCalled() - }) + expect(driver.navigate).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { - driver.navigate.mockRejectedValue( - new Error("M_UNKNOWN: Unknown error"), - ) + driver.navigate.mockRejectedValue(new Error("M_UNKNOWN: Unknown error")); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, @@ -263,27 +254,27 @@ describe("ClientWidgetApi", () => { data: { uri: "https://matrix.to/#/#room:example.net", }, - } + }; - await loadIframe(["org.matrix.msc2931.navigate"]) + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Error handling navigation" }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.navigate.mockRejectedValue( new CustomMatrixError("failed to navigate", 400, "M_UNKNOWN", { reason: "Unknown error", }), - ) + ); const event: INavigateActionRequest = { api: WidgetApiDirection.FromWidget, @@ -293,11 +284,11 @@ describe("ClientWidgetApi", () => { data: { uri: "https://matrix.to/#/#room:example.net", }, - } + }; - await loadIframe(["org.matrix.msc2931.navigate"]) + await loadIframe(["org.matrix.msc2931.navigate"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -314,20 +305,20 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("send_event action", () => { it("sends message events", async () => { - const roomId = "!room:example.org" - const eventId = "$event:example.org" + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; driver.sendEvent.mockResolvedValue({ roomId, eventId, - }) + }); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -339,38 +330,33 @@ describe("ClientWidgetApi", () => { content: {}, room_id: roomId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { room_id: roomId, event_id: eventId, - }) - }) + }); + }); - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - null, - roomId, - ) - }) + expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, null, roomId); + }); it("sends state events", async () => { - const roomId = "!room:example.org" - const eventId = "$event:example.org" + const roomId = "!room:example.org"; + const eventId = "$event:example.org"; driver.sendEvent.mockResolvedValue({ roomId, eventId, - }) + }); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -383,36 +369,29 @@ describe("ClientWidgetApi", () => { state_key: "", room_id: roomId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.state_event:${event.data.type}`, - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { room_id: roomId, event_id: eventId, - }) - }) + }); + }); - expect(driver.sendEvent).toHaveBeenCalledWith( - event.data.type, - event.data.content, - "", - roomId, - ) - }) + expect(driver.sendEvent).toHaveBeenCalledWith(event.data.type, event.data.content, "", roomId); + }); it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; - driver.sendEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ) + driver.sendEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -424,37 +403,32 @@ describe("ClientWidgetApi", () => { content: "hello", room_id: roomId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Error sending event" }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.sendEvent.mockRejectedValue( - new CustomMatrixError( - "failed to send event", - 400, - "M_NOT_JSON", - { - reason: "Content must be a JSON object.", - }, - ), - ) + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", + }), + ); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -466,14 +440,14 @@ describe("ClientWidgetApi", () => { content: "hello", room_id: roomId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -490,14 +464,14 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("send_event action for delayed events", () => { it("fails to send delayed events", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -510,34 +484,34 @@ describe("ClientWidgetApi", () => { delay: 5000, room_id: roomId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, // Without the required capability - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: expect.any(String) }, - }) - }) + }); + }); - expect(driver.sendDelayedEvent).not.toBeCalled() - }) + expect(driver.sendDelayedEvent).not.toBeCalled(); + }); it("sends delayed message events", async () => { - const roomId = "!room:example.org" - const parentDelayId = "fp" - const timeoutDelayId = "ft" + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ roomId, delayId: timeoutDelayId, - }) + }); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -551,22 +525,22 @@ describe("ClientWidgetApi", () => { delay: 5000, parent_delay_id: parentDelayId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, "org.matrix.msc4157.send.delayed_event", - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { room_id: roomId, delay_id: timeoutDelayId, - }) - }) + }); + }); expect(driver.sendDelayedEvent).toHaveBeenCalledWith( event.data.delay, @@ -575,18 +549,18 @@ describe("ClientWidgetApi", () => { event.data.content, null, roomId, - ) - }) + ); + }); it("sends delayed state events", async () => { - const roomId = "!room:example.org" - const parentDelayId = "fp" - const timeoutDelayId = "ft" + const roomId = "!room:example.org"; + const parentDelayId = "fp"; + const timeoutDelayId = "ft"; driver.sendDelayedEvent.mockResolvedValue({ roomId, delayId: timeoutDelayId, - }) + }); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -601,22 +575,22 @@ describe("ClientWidgetApi", () => { delay: 5000, parent_delay_id: parentDelayId, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.state_event:${event.data.type}`, "org.matrix.msc4157.send.delayed_event", - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { room_id: roomId, delay_id: timeoutDelayId, - }) - }) + }); + }); expect(driver.sendDelayedEvent).toHaveBeenCalledWith( event.data.delay, @@ -625,15 +599,13 @@ describe("ClientWidgetApi", () => { event.data.content, "", roomId, - ) - }) + ); + }); it("should reject requests when the driver throws an exception", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; - driver.sendDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ) + driver.sendDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -647,38 +619,33 @@ describe("ClientWidgetApi", () => { delay: 5000, parent_delay_id: "fp", }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, "org.matrix.msc4157.send.delayed_event", - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Error sending event" }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.sendDelayedEvent.mockRejectedValue( - new CustomMatrixError( - "failed to send event", - 400, - "M_NOT_JSON", - { - reason: "Content must be a JSON object.", - }, - ), - ) + new CustomMatrixError("failed to send event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", + }), + ); const event: ISendEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -692,15 +659,15 @@ describe("ClientWidgetApi", () => { delay: 5000, parent_delay_id: "fp", }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${event.data.room_id}`, `org.matrix.msc2762.send.event:${event.data.type}`, "org.matrix.msc4157.send.delayed_event", - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -717,163 +684,132 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("receiving events", () => { - const roomId = "!room:example.org" - const otherRoomId = "!other-room:example.org" + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; const event = createRoomEvent({ room_id: roomId, type: "m.room.message", content: "hello", - }) + }); const eventFromOtherRoom = createRoomEvent({ room_id: otherRoomId, type: "m.room.message", content: "test", - }) + }); it("forwards events to the widget from one room only", async () => { // Give the widget capabilities to receive from just one room await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, "org.matrix.msc2762.receive.event:m.room.message", - ]) + ]); // Event from the matching room should be forwarded - clientWidgetApi.feedEvent(event) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ) + clientWidgetApi.feedEvent(event); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom) - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ) - }) + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); + }); it("forwards events to the widget from the currently viewed room", async () => { - clientWidgetApi.setViewedRoomId(roomId) + clientWidgetApi.setViewedRoomId(roomId); // Give the widget capabilities to receive events without specifying // any rooms that it can read await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, "org.matrix.msc2762.receive.event:m.room.message", - ]) + ]); // Event from the viewed room should be forwarded - clientWidgetApi.feedEvent(event) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ) + clientWidgetApi.feedEvent(event); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); // Event from the other room should not be forwarded - clientWidgetApi.feedEvent(eventFromOtherRoom) - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ) + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); // View the other room; now the event can be forwarded - clientWidgetApi.setViewedRoomId(otherRoomId) - clientWidgetApi.feedEvent(eventFromOtherRoom) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ) - }) + clientWidgetApi.setViewedRoomId(otherRoomId); + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); + }); it("forwards events to the widget from all rooms", async () => { // Give the widget capabilities to receive from any known room await loadIframe([ `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, "org.matrix.msc2762.receive.event:m.room.message", - ]) + ]); // Events from both rooms should be forwarded - clientWidgetApi.feedEvent(event) - clientWidgetApi.feedEvent(eventFromOtherRoom) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - event, - ) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.SendEvent, - eventFromOtherRoom, - ) - }) - }) + clientWidgetApi.feedEvent(event); + clientWidgetApi.feedEvent(eventFromOtherRoom); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, event); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.SendEvent, eventFromOtherRoom); + }); + }); describe("receiving room state", () => { it("syncs initial state and feeds updates", async () => { - const roomId = "!room:example.org" - const otherRoomId = "!other-room:example.org" - clientWidgetApi.setViewedRoomId(roomId) + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; + clientWidgetApi.setViewedRoomId(roomId); const topicEvent = createRoomEvent({ room_id: roomId, type: "m.room.topic", state_key: "", content: { topic: "Hello world!" }, - }) + }); const nameEvent = createRoomEvent({ room_id: roomId, type: "m.room.name", state_key: "", content: { name: "Test room" }, - }) + }); const joinRulesEvent = createRoomEvent({ room_id: roomId, type: "m.room.join_rules", state_key: "", content: { join_rule: "public" }, - }) + }); const otherRoomNameEvent = createRoomEvent({ room_id: otherRoomId, type: "m.room.name", state_key: "", content: { name: "Other room" }, - }) + }); // Artificially delay the delivery of the join rules event - let resolveJoinRules: () => void - const joinRules = new Promise( - (resolve) => (resolveJoinRules = resolve), - ) - - driver.readRoomState.mockImplementation( - async (rId, eventType, stateKey) => { - if (rId === roomId) { - if (eventType === "m.room.topic" && stateKey === "") - return [topicEvent] - if (eventType === "m.room.name" && stateKey === "") - return [nameEvent] - if ( - eventType === "m.room.join_rules" && - stateKey === "" - ) { - await joinRules - return [joinRulesEvent] - } - } else if (rId === otherRoomId) { - if (eventType === "m.room.name" && stateKey === "") - return [otherRoomNameEvent] + let resolveJoinRules: () => void; + const joinRules = new Promise((resolve) => (resolveJoinRules = resolve)); + + driver.readRoomState.mockImplementation(async (rId, eventType, stateKey) => { + if (rId === roomId) { + if (eventType === "m.room.topic" && stateKey === "") return [topicEvent]; + if (eventType === "m.room.name" && stateKey === "") return [nameEvent]; + if (eventType === "m.room.join_rules" && stateKey === "") { + await joinRules; + return [joinRulesEvent]; } - return [] - }, - ) + } else if (rId === otherRoomId) { + if (eventType === "m.room.name" && stateKey === "") return [otherRoomNameEvent]; + } + return []; + }); await loadIframe([ "org.matrix.msc2762.receive.state_event:m.room.topic#", "org.matrix.msc2762.receive.state_event:m.room.name#", "org.matrix.msc2762.receive.state_event:m.room.join_rules#", - ]) + ]); // Simulate a race between reading the original join rules event and // the join rules being updated at the same time @@ -882,28 +818,22 @@ describe("ClientWidgetApi", () => { type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" }, - }) - clientWidgetApi.feedStateUpdate(newJoinRulesEvent) + }); + clientWidgetApi.feedStateUpdate(newJoinRulesEvent); // What happens if the original join rules are delivered after the // updated ones? - resolveJoinRules!() + resolveJoinRules!(); await waitFor(() => { // The initial topic and name should have been pushed - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: [topicEvent, nameEvent, newJoinRulesEvent], - }, - ) + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: [topicEvent, nameEvent, newJoinRulesEvent], + }); // Only the updated join rules should have been delivered - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([joinRules]), - }, - ) - }) + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([joinRules]), + }); + }); // Check that further updates to room state are pushed to the widget // as expected @@ -912,41 +842,32 @@ describe("ClientWidgetApi", () => { type: "m.room.topic", state_key: "", content: { topic: "Our new topic" }, - }) - clientWidgetApi.feedStateUpdate(newTopicEvent) + }); + clientWidgetApi.feedStateUpdate(newTopicEvent); await waitFor(() => { - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: [newTopicEvent], - }, - ) - }) + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: [newTopicEvent], + }); + }); // Up to this point we should not have received any state for the // other (unviewed) room - expect(transport.send).not.toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([otherRoomNameEvent]), - }, - ) + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([otherRoomNameEvent]), + }); // Now view the other room - clientWidgetApi.setViewedRoomId(otherRoomId) - ;(transport.send as unknown as jest.SpyInstance).mockClear() + clientWidgetApi.setViewedRoomId(otherRoomId); + (transport.send as unknown as jest.SpyInstance).mockClear(); await waitFor(() => { // The state of the other room should now be pushed - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.UpdateState, - { - state: expect.arrayContaining([otherRoomNameEvent]), - }, - ) - }) - }) - }) + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState, { + state: expect.arrayContaining([otherRoomNameEvent]), + }); + }); + }); + }); describe("update_delayed_event action", () => { it("fails to update delayed events", async () => { @@ -959,20 +880,20 @@ describe("ClientWidgetApi", () => { delay_id: "f", action: UpdateDelayedEventAction.Send, }, - } + }; - await loadIframe([]) // Without the required capability + await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: expect.any(String) }, - }) - }) + }); + }); - expect(driver.updateDelayedEvent).not.toBeCalled() - }) + expect(driver.updateDelayedEvent).not.toBeCalled(); + }); it("fails to update delayed events with unsupported action", async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = { @@ -984,23 +905,23 @@ describe("ClientWidgetApi", () => { delay_id: "f", action: "unknown" as UpdateDelayedEventAction, }, - } + }; - await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: expect.any(String) }, - }) - }) + }); + }); - expect(driver.updateDelayedEvent).not.toBeCalled() - }) + expect(driver.updateDelayedEvent).not.toBeCalled(); + }); it("updates delayed events", async () => { - driver.updateDelayedEvent.mockResolvedValue(undefined) + driver.updateDelayedEvent.mockResolvedValue(undefined); for (const action of [ UpdateDelayedEventAction.Cancel, @@ -1016,27 +937,22 @@ describe("ClientWidgetApi", () => { delay_id: "f", action, }, - } + }; - await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}) - }) + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); - expect(driver.updateDelayedEvent).toHaveBeenCalledWith( - event.data.delay_id, - event.data.action, - ) + expect(driver.updateDelayedEvent).toHaveBeenCalledWith(event.data.delay_id, event.data.action); } - }) + }); it("should reject requests when the driver throws an exception", async () => { - driver.updateDelayedEvent.mockRejectedValue( - new Error("M_BAD_JSON: Content must be a JSON object"), - ) + driver.updateDelayedEvent.mockRejectedValue(new Error("M_BAD_JSON: Content must be a JSON object")); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1047,32 +963,27 @@ describe("ClientWidgetApi", () => { delay_id: "f", action: UpdateDelayedEventAction.Send, }, - } + }; - await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Error updating delayed event" }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.updateDelayedEvent.mockRejectedValue( - new CustomMatrixError( - "failed to update delayed event", - 400, - "M_NOT_JSON", - { - reason: "Content must be a JSON object.", - }, - ), - ) + new CustomMatrixError("failed to update delayed event", 400, "M_NOT_JSON", { + reason: "Content must be a JSON object.", + }), + ); const event: IUpdateDelayedEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1083,11 +994,11 @@ describe("ClientWidgetApi", () => { delay_id: "f", action: UpdateDelayedEventAction.Send, }, - } + }; - await loadIframe(["org.matrix.msc4157.update_delayed_event"]) + await loadIframe(["org.matrix.msc4157.update_delayed_event"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1104,10 +1015,10 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("send_to_device action", () => { it("sends unencrypted to-device events", async () => { @@ -1127,24 +1038,22 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { - expect(transport.reply).toHaveBeenCalledWith(event, {}) - }) + expect(transport.reply).toHaveBeenCalledWith(event, {}); + }); expect(driver.sendToDevice).toHaveBeenCalledWith( event.data.type, event.data.encrypted, event.data.messages, - ) - }) + ); + }); it("fails to send to-device events without event type", async () => { const event: IWidgetApiRequest = { @@ -1162,22 +1071,20 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - missing event type" }, - }) - }) + }); + }); - expect(driver.sendToDevice).not.toBeCalled() - }) + expect(driver.sendToDevice).not.toBeCalled(); + }); it("fails to send to-device events without event contents", async () => { const event: IWidgetApiRequest = { @@ -1189,24 +1096,22 @@ describe("ClientWidgetApi", () => { type: "net.example.test", encrypted: false, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - missing event contents", }, - }) - }) + }); + }); - expect(driver.sendToDevice).not.toBeCalled() - }) + expect(driver.sendToDevice).not.toBeCalled(); + }); it("fails to send to-device events without encryption flag", async () => { const event: IWidgetApiRequest = { @@ -1224,24 +1129,22 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - missing encryption flag", }, - }) - }) + }); + }); - expect(driver.sendToDevice).not.toBeCalled() - }) + expect(driver.sendToDevice).not.toBeCalled(); + }); it("fails to send to-device events with any event type", async () => { const event: ISendToDeviceFromWidgetActionRequest = { @@ -1260,31 +1163,27 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}_different`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}_different`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Cannot send to-device events of this type", }, - }) - }) + }); + }); - expect(driver.sendToDevice).not.toBeCalled() - }) + expect(driver.sendToDevice).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { driver.sendToDevice.mockRejectedValue( - new Error( - "M_FORBIDDEN: You don't have permission to send to-device events", - ), - ) + new Error("M_FORBIDDEN: You don't have permission to send to-device events"), + ); const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1302,34 +1201,27 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Error sending event" }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.sendToDevice.mockRejectedValue( - new CustomMatrixError( - "failed to send event", - 400, - "M_FORBIDDEN", - { - reason: "You don't have permission to send to-device events", - }, - ), - ) + new CustomMatrixError("failed to send event", 400, "M_FORBIDDEN", { + reason: "You don't have permission to send to-device events", + }), + ); const event: ISendToDeviceFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1347,13 +1239,11 @@ describe("ClientWidgetApi", () => { }, }, }, - } + }; - await loadIframe([ - `org.matrix.msc3819.send.to_device:${event.data.type}`, - ]) + await loadIframe([`org.matrix.msc3819.send.to_device:${event.data.type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1370,10 +1260,10 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("get_openid action", () => { it("gets info", async () => { @@ -1383,8 +1273,8 @@ describe("ClientWidgetApi", () => { token: { access_token: "access_token", }, - }) - }) + }); + }); const event: IGetOpenIDActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1392,30 +1282,28 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.GetOpenIDCredentials, data: {}, - } + }; - await loadIframe([]) + await loadIframe([]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { state: OpenIDRequestState.Allowed, access_token: "access_token", - }) - }) + }); + }); - expect(driver.askOpenID).toHaveBeenCalledWith( - expect.any(SimpleObservable), - ) - }) + expect(driver.askOpenID).toHaveBeenCalledWith(expect.any(SimpleObservable)); + }); it("fails when client provided invalid token", async () => { driver.askOpenID.mockImplementation((observable) => { observable.update({ state: OpenIDRequestState.Allowed, - }) - }) + }); + }); const event: IGetOpenIDActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1423,31 +1311,28 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.GetOpenIDCredentials, data: {}, - } + }; - await loadIframe([]) + await loadIframe([]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { error: { - message: - "client provided invalid OIDC token for an allowed request", + message: "client provided invalid OIDC token for an allowed request", }, - }) - }) + }); + }); - expect(driver.askOpenID).toHaveBeenCalledWith( - expect.any(SimpleObservable), - ) - }) - }) + expect(driver.askOpenID).toHaveBeenCalledWith(expect.any(SimpleObservable)); + }); + }); describe("com.beeper.read_room_account_data action", () => { it("reads room account data", async () => { - const type = "net.example.test" - const roomId = "!room:example.org" + const type = "net.example.test"; + const roomId = "!room:example.org"; driver.readRoomAccountData.mockResolvedValue([ { @@ -1455,7 +1340,7 @@ describe("ClientWidgetApi", () => { room_id: roomId, content: {}, }, - ]) + ]); const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1466,13 +1351,11 @@ describe("ClientWidgetApi", () => { room_ids: [roomId], type, }, - } + }; - await loadIframe([ - `com.beeper.capabilities.receive.room_account_data:${type}`, - ]) + await loadIframe([`com.beeper.capabilities.receive.room_account_data:${type}`]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { @@ -1483,17 +1366,15 @@ describe("ClientWidgetApi", () => { content: {}, }, ], - }) - }) + }); + }); - expect(driver.readRoomAccountData).toHaveBeenCalledWith( - event.data.type, - ) - }) + expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); + }); it("does not read room account data", async () => { - const type = "net.example.test" - const roomId = "!room:example.org" + const type = "net.example.test"; + const roomId = "!room:example.org"; driver.readRoomAccountData.mockResolvedValue([ { @@ -1501,7 +1382,7 @@ describe("ClientWidgetApi", () => { room_id: roomId, content: {}, }, - ]) + ]); const event: IReadRoomAccountDataFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1512,38 +1393,36 @@ describe("ClientWidgetApi", () => { room_ids: [roomId], type, }, - } + }; - await loadIframe([]) // Without the required capability + await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { error: { message: "Cannot read room account data of this type", }, - }) - }) + }); + }); - expect(driver.readRoomAccountData).toHaveBeenCalledWith( - event.data.type, - ) - }) - }) + expect(driver.readRoomAccountData).toHaveBeenCalledWith(event.data.type); + }); + }); describe("org.matrix.msc2876.read_events action", () => { it("reads events from a specific room", async () => { - const roomId = "!room:example.org" + const roomId = "!room:example.org"; const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test", - }) + }); driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event] - return [] - }) + if (rId === roomId) return [event]; + return []; + }); const request: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1554,21 +1433,21 @@ describe("ClientWidgetApi", () => { type: "net.example.test", room_ids: [roomId], }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${roomId}`, "org.matrix.msc2762.receive.event:net.example.test", - ]) - clientWidgetApi.setViewedRoomId(roomId) + ]); + clientWidgetApi.setViewedRoomId(roomId); - emitEvent(new CustomEvent("", { detail: request })) + emitEvent(new CustomEvent("", { detail: request })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(request, { events: [event], - }) - }) + }); + }); expect(driver.readRoomTimeline).toHaveBeenCalledWith( roomId, @@ -1577,28 +1456,28 @@ describe("ClientWidgetApi", () => { undefined, 0, undefined, - ) - }) + ); + }); it("reads events from all rooms", async () => { - const roomId = "!room:example.org" - const otherRoomId = "!other-room:example.org" + const roomId = "!room:example.org"; + const otherRoomId = "!other-room:example.org"; const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test", - }) + }); const otherRoomEvent = createRoomEvent({ room_id: otherRoomId, type: "net.example.test", content: "hi", - }) - driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]) + }); + driver.getKnownRooms.mockReturnValue([roomId, otherRoomId]); driver.readRoomTimeline.mockImplementation(async (rId) => { - if (rId === roomId) return [event] - if (rId === otherRoomId) return [otherRoomEvent] - return [] - }) + if (rId === roomId) return [event]; + if (rId === otherRoomId) return [otherRoomEvent]; + return []; + }); const request: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1609,21 +1488,21 @@ describe("ClientWidgetApi", () => { type: "net.example.test", room_ids: Symbols.AnyRoom, }, - } + }; await loadIframe([ `org.matrix.msc2762.timeline:${Symbols.AnyRoom}`, "org.matrix.msc2762.receive.event:net.example.test", - ]) - clientWidgetApi.setViewedRoomId(roomId) + ]); + clientWidgetApi.setViewedRoomId(roomId); - emitEvent(new CustomEvent("", { detail: request })) + emitEvent(new CustomEvent("", { detail: request })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(request, { events: [event, otherRoomEvent], - }) - }) + }); + }); expect(driver.readRoomTimeline).toHaveBeenCalledWith( roomId, @@ -1632,7 +1511,7 @@ describe("ClientWidgetApi", () => { undefined, 0, undefined, - ) + ); expect(driver.readRoomTimeline).toHaveBeenCalledWith( otherRoomId, "net.example.test", @@ -1640,14 +1519,14 @@ describe("ClientWidgetApi", () => { undefined, 0, undefined, - ) - }) + ); + }); it("reads state events with any state key", async () => { driver.readRoomTimeline.mockResolvedValue([ createRoomEvent({ type: "net.example.test", state_key: "A" }), createRoomEvent({ type: "net.example.test", state_key: "B" }), - ]) + ]); const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1658,14 +1537,12 @@ describe("ClientWidgetApi", () => { type: "net.example.test", state_key: true, }, - } + }; - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test", - ]) - clientWidgetApi.setViewedRoomId("!room-id") + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test"]); + clientWidgetApi.setViewedRoomId("!room-id"); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1679,8 +1556,8 @@ describe("ClientWidgetApi", () => { state_key: "B", }), ], - }) - }) + }); + }); expect(driver.readRoomTimeline).toBeCalledWith( "!room-id", @@ -1689,8 +1566,8 @@ describe("ClientWidgetApi", () => { undefined, 0, undefined, - ) - }) + ); + }); it("fails to read state events with any state key", async () => { const event: IReadEventFromWidgetActionRequest = { @@ -1702,25 +1579,23 @@ describe("ClientWidgetApi", () => { type: "net.example.test", state_key: true, }, - } + }; - await loadIframe([]) // Without the required capability + await loadIframe([]); // Without the required capability - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: expect.any(String) }, - }) - }) + }); + }); - expect(driver.readRoomTimeline).not.toBeCalled() - }) + expect(driver.readRoomTimeline).not.toBeCalled(); + }); it("reads state events with a specific state key", async () => { - driver.readRoomTimeline.mockResolvedValue([ - createRoomEvent({ type: "net.example.test", state_key: "B" }), - ]) + driver.readRoomTimeline.mockResolvedValue([createRoomEvent({ type: "net.example.test", state_key: "B" })]); const event: IReadEventFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1731,14 +1606,12 @@ describe("ClientWidgetApi", () => { type: "net.example.test", state_key: "B", }, - } + }; - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test#B", - ]) - clientWidgetApi.setViewedRoomId("!room-id") + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#B"]); + clientWidgetApi.setViewedRoomId("!room-id"); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1748,8 +1621,8 @@ describe("ClientWidgetApi", () => { state_key: "B", }), ], - }) - }) + }); + }); expect(driver.readRoomTimeline).toBeCalledWith( "!room-id", @@ -1758,8 +1631,8 @@ describe("ClientWidgetApi", () => { "B", 0, undefined, - ) - }) + ); + }); it("fails to read state events with a specific state key", async () => { const event: IReadEventFromWidgetActionRequest = { @@ -1771,24 +1644,22 @@ describe("ClientWidgetApi", () => { type: "net.example.test", state_key: "B", }, - } + }; // Request the capability for the wrong state key - await loadIframe([ - "org.matrix.msc2762.receive.state_event:net.example.test#A", - ]) + await loadIframe(["org.matrix.msc2762.receive.state_event:net.example.test#A"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: expect.any(String) }, - }) - }) + }); + }); - expect(driver.readRoomTimeline).not.toBeCalled() - }) - }) + expect(driver.readRoomTimeline).not.toBeCalled(); + }); + }); describe("org.matrix.msc3869.read_relations action", () => { it("should present as supported api version", () => { @@ -1798,21 +1669,19 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3869, - ]), - }) - }) + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3869]), + }); + }); it("should handle and process the request", async () => { driver.readEventRelations.mockResolvedValue({ chunk: [createRoomEvent()], - }) + }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1820,19 +1689,17 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { event_id: "$event" }, - } + }; - await loadIframe([ - "org.matrix.msc2762.receive.event:m.room.message", - ]) + await loadIframe(["org.matrix.msc2762.receive.event:m.room.message"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { chunk: [createRoomEvent()], - }) - }) + }); + }); expect(driver.readEventRelations).toBeCalledWith( "$event", @@ -1843,8 +1710,8 @@ describe("ClientWidgetApi", () => { undefined, undefined, undefined, - ) - }) + ); + }); it("should only return events that match requested capabilities", async () => { driver.readEventRelations.mockResolvedValue({ @@ -1860,7 +1727,7 @@ describe("ClientWidgetApi", () => { state_key: "B", }), ], - }) + }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1868,14 +1735,14 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { event_id: "$event" }, - } + }; await loadIframe([ "org.matrix.msc2762.receive.event:m.room.message", "org.matrix.msc2762.receive.state_event:net.example.test#A", - ]) + ]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -1886,8 +1753,8 @@ describe("ClientWidgetApi", () => { state_key: "A", }), ], - }) - }) + }); + }); expect(driver.readEventRelations).toBeCalledWith( "$event", @@ -1898,13 +1765,13 @@ describe("ClientWidgetApi", () => { undefined, undefined, undefined, - ) - }) + ); + }); it("should accept all options and pass it to the driver", async () => { driver.readEventRelations.mockResolvedValue({ chunk: [], - }) + }); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -1921,17 +1788,17 @@ describe("ClientWidgetApi", () => { to: "to-token", direction: "f", }, - } + }; - await loadIframe(["org.matrix.msc2762.timeline:!room-id"]) + await loadIframe(["org.matrix.msc2762.timeline:!room-id"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { chunk: [], - }) - }) + }); + }); expect(driver.readEventRelations).toBeCalledWith( "$event", @@ -1942,8 +1809,8 @@ describe("ClientWidgetApi", () => { "to-token", 25, "f", - ) - }) + ); + }); it("should reject requests without event_id", async () => { const event: IWidgetApiRequest = { @@ -1952,14 +1819,14 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - missing event ID" }, - }) - }) + }); + }); it("should reject requests with a negative limit", async () => { const event: IReadRelationsFromWidgetActionRequest = { @@ -1971,14 +1838,14 @@ describe("ClientWidgetApi", () => { event_id: "$event", limit: -1, }, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - limit out of range" }, - }) - }) + }); + }); it("should reject requests when the room timeline was not requested", async () => { const event: IReadRelationsFromWidgetActionRequest = { @@ -1990,23 +1857,21 @@ describe("ClientWidgetApi", () => { event_id: "$event", room_id: "!another-room-id", }, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Unable to access room timeline: !another-room-id", }, - }) - }) + }); + }); it("should reject requests when the driver throws an exception", async () => { driver.readEventRelations.mockRejectedValue( - new Error( - "M_FORBIDDEN: You don't have permission to access that event", - ), - ) + new Error("M_FORBIDDEN: You don't have permission to access that event"), + ); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2014,34 +1879,29 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { event_id: "$event" }, - } + }; - await loadIframe() + await loadIframe(); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Unexpected error while reading relations", }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.readEventRelations.mockRejectedValue( - new CustomMatrixError( - "failed to read relations", - 403, - "M_FORBIDDEN", - { - reason: "You don't have permission to access that event", - }, - ), - ) + new CustomMatrixError("failed to read relations", 403, "M_FORBIDDEN", { + reason: "You don't have permission to access that event", + }), + ); const event: IReadRelationsFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2049,11 +1909,11 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { event_id: "$event" }, - } + }; - await loadIframe() + await loadIframe(); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2070,10 +1930,10 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("org.matrix.msc3973.user_directory_search action", () => { it("should present as supported api version", () => { @@ -2083,16 +1943,14 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC3973, - ]), - }) - }) + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC3973]), + }); + }); it("should handle and process the request", async () => { driver.searchUserDirectory.mockResolvedValue({ @@ -2102,7 +1960,7 @@ describe("ClientWidgetApi", () => { userId: "@foo:bar.com", }, ], - }) + }); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2110,11 +1968,11 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "foo" }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2126,11 +1984,11 @@ describe("ClientWidgetApi", () => { avatar_url: undefined, }, ], - }) - }) + }); + }); - expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined) - }) + expect(driver.searchUserDirectory).toBeCalledWith("foo", undefined); + }); it("should accept all options and pass it to the driver", async () => { driver.searchUserDirectory.mockResolvedValue({ @@ -2145,7 +2003,7 @@ describe("ClientWidgetApi", () => { avatarUrl: "mxc://...", }, ], - }) + }); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2156,11 +2014,11 @@ describe("ClientWidgetApi", () => { search_term: "foo", limit: 5, }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2177,17 +2035,17 @@ describe("ClientWidgetApi", () => { avatar_url: "mxc://...", }, ], - }) - }) + }); + }); - expect(driver.searchUserDirectory).toBeCalledWith("foo", 5) - }) + expect(driver.searchUserDirectory).toBeCalledWith("foo", 5); + }); it("should accept empty search_term", async () => { driver.searchUserDirectory.mockResolvedValue({ limited: false, results: [], - }) + }); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2195,21 +2053,21 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "" }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { limited: false, results: [], - }) - }) + }); + }); - expect(driver.searchUserDirectory).toBeCalledWith("", undefined) - }) + expect(driver.searchUserDirectory).toBeCalledWith("", undefined); + }); it("should reject requests when the capability was not requested", async () => { const event: IUserDirectorySearchFromWidgetActionRequest = { @@ -2218,16 +2076,16 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "foo" }, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Missing capability" }, - }) + }); - expect(driver.searchUserDirectory).not.toBeCalled() - }) + expect(driver.searchUserDirectory).not.toBeCalled(); + }); it("should reject requests without search_term", async () => { const event: IWidgetApiRequest = { @@ -2236,18 +2094,18 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: {}, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - missing search term" }, - }) + }); - expect(driver.searchUserDirectory).not.toBeCalled() - }) + expect(driver.searchUserDirectory).not.toBeCalled(); + }); it("should reject requests with a negative limit", async () => { const event: IUserDirectorySearchFromWidgetActionRequest = { @@ -2259,23 +2117,21 @@ describe("ClientWidgetApi", () => { search_term: "foo", limit: -1, }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Invalid request - limit out of range" }, - }) + }); - expect(driver.searchUserDirectory).not.toBeCalled() - }) + expect(driver.searchUserDirectory).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { - driver.searchUserDirectory.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ) + driver.searchUserDirectory.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2283,36 +2139,30 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "foo" }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: - "Unexpected error while searching in the user directory", + message: "Unexpected error while searching in the user directory", }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.searchUserDirectory.mockRejectedValue( - new CustomMatrixError( - "failed to search the user directory", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ) + new CustomMatrixError("failed to search the user directory", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", + retry_after_ms: 2000, + }), + ); const event: IUserDirectorySearchFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2320,17 +2170,16 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "foo" }, - } + }; - await loadIframe(["org.matrix.msc3973.user_directory_search"]) + await loadIframe(["org.matrix.msc3973.user_directory_search"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: - "Unexpected error while searching in the user directory", + message: "Unexpected error while searching in the user directory", matrix_api_error: { http_status: 429, http_headers: {}, @@ -2343,10 +2192,10 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("org.matrix.msc4039.get_media_config action", () => { it("should present as supported api version", () => { @@ -2356,21 +2205,19 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), - }) - }) + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), + }); + }); it("should handle and process the request", async () => { driver.getMediaConfig.mockResolvedValue({ "m.upload.size": 1000, - }) + }); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2378,20 +2225,20 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { "m.upload.size": 1000, - }) - }) + }); + }); - expect(driver.getMediaConfig).toBeCalled() - }) + expect(driver.getMediaConfig).toBeCalled(); + }); it("should reject requests when the capability was not requested", async () => { const event: IGetMediaConfigActionFromWidgetActionRequest = { @@ -2400,21 +2247,19 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Missing capability" }, - }) + }); - expect(driver.getMediaConfig).not.toBeCalled() - }) + expect(driver.getMediaConfig).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { - driver.getMediaConfig.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ) + driver.getMediaConfig.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2422,36 +2267,30 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: - "Unexpected error while getting the media configuration", + message: "Unexpected error while getting the media configuration", }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.getMediaConfig.mockRejectedValue( - new CustomMatrixError( - "failed to get the media configuration", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ) + new CustomMatrixError("failed to get the media configuration", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", + retry_after_ms: 2000, + }), + ); const event: IGetMediaConfigActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2459,17 +2298,16 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { - message: - "Unexpected error while getting the media configuration", + message: "Unexpected error while getting the media configuration", matrix_api_error: { http_status: 429, http_headers: {}, @@ -2482,10 +2320,10 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("MSC4039", () => { it("should present as supported api version", () => { @@ -2495,23 +2333,21 @@ describe("ClientWidgetApi", () => { requestId: "0", action: WidgetApiFromWidgetAction.SupportedApiVersions, data: {}, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { - supported_versions: expect.arrayContaining([ - UnstableApiVersion.MSC4039, - ]), - }) - }) - }) + supported_versions: expect.arrayContaining([UnstableApiVersion.MSC4039]), + }); + }); + }); describe("org.matrix.msc4039.upload_file action", () => { it("should handle and process the request", async () => { driver.uploadFile.mockResolvedValue({ contentUri: "mxc://...", - }) + }); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2521,20 +2357,20 @@ describe("ClientWidgetApi", () => { data: { file: "data", }, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { content_uri: "mxc://...", - }) - }) + }); + }); - expect(driver.uploadFile).toBeCalled() - }) + expect(driver.uploadFile).toBeCalled(); + }); it("should reject requests when the capability was not requested", async () => { const event: IUploadFileActionFromWidgetActionRequest = { @@ -2545,21 +2381,19 @@ describe("ClientWidgetApi", () => { data: { file: "data", }, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Missing capability" }, - }) + }); - expect(driver.uploadFile).not.toBeCalled() - }) + expect(driver.uploadFile).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { - driver.uploadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ) + driver.uploadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2569,35 +2403,30 @@ describe("ClientWidgetApi", () => { data: { file: "data", }, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Unexpected error while uploading a file", }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.uploadFile.mockRejectedValue( - new CustomMatrixError( - "failed to upload a file", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ) + new CustomMatrixError("failed to upload a file", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", + retry_after_ms: 2000, + }), + ); const event: IUploadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2607,11 +2436,11 @@ describe("ClientWidgetApi", () => { data: { file: "data", }, - } + }; - await loadIframe(["org.matrix.msc4039.upload_file"]) + await loadIframe(["org.matrix.msc4039.upload_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2629,16 +2458,16 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); describe("org.matrix.msc4039.download_file action", () => { it("should handle and process the request", async () => { driver.downloadFile.mockResolvedValue({ file: "test contents", - }) + }); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2648,22 +2477,20 @@ describe("ClientWidgetApi", () => { data: { content_uri: "mxc://example.com/test_file", }, - } + }; - await loadIframe(["org.matrix.msc4039.download_file"]) + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toHaveBeenCalledWith(event, { file: "test contents", - }) - }) + }); + }); - expect(driver.downloadFile).toHaveBeenCalledWith( - "mxc://example.com/test_file", - ) - }) + expect(driver.downloadFile).toHaveBeenCalledWith("mxc://example.com/test_file"); + }); it("should reject requests when the capability was not requested", async () => { const event: IDownloadFileActionFromWidgetActionRequest = { @@ -2674,21 +2501,19 @@ describe("ClientWidgetApi", () => { data: { content_uri: "mxc://example.com/test_file", }, - } + }; - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); expect(transport.reply).toBeCalledWith(event, { error: { message: "Missing capability" }, - }) + }); - expect(driver.uploadFile).not.toBeCalled() - }) + expect(driver.uploadFile).not.toBeCalled(); + }); it("should reject requests when the driver throws an exception", async () => { - driver.downloadFile.mockRejectedValue( - new Error("M_LIMIT_EXCEEDED: Too many requests"), - ) + driver.downloadFile.mockRejectedValue(new Error("M_LIMIT_EXCEEDED: Too many requests")); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2698,35 +2523,30 @@ describe("ClientWidgetApi", () => { data: { content_uri: "mxc://example.com/test_file", }, - } + }; - await loadIframe(["org.matrix.msc4039.download_file"]) + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { error: { message: "Unexpected error while downloading a file", }, - }) - }) - }) + }); + }); + }); it("should reject with Matrix API error response thrown by driver", async () => { - driver.processError.mockImplementation(processCustomMatrixError) + driver.processError.mockImplementation(processCustomMatrixError); driver.downloadFile.mockRejectedValue( - new CustomMatrixError( - "failed to download a file", - 429, - "M_LIMIT_EXCEEDED", - { - reason: "Too many requests", - retry_after_ms: 2000, - }, - ), - ) + new CustomMatrixError("failed to download a file", 429, "M_LIMIT_EXCEEDED", { + reason: "Too many requests", + retry_after_ms: 2000, + }), + ); const event: IDownloadFileActionFromWidgetActionRequest = { api: WidgetApiDirection.FromWidget, @@ -2736,11 +2556,11 @@ describe("ClientWidgetApi", () => { data: { content_uri: "mxc://example.com/test_file", }, - } + }; - await loadIframe(["org.matrix.msc4039.download_file"]) + await loadIframe(["org.matrix.msc4039.download_file"]); - emitEvent(new CustomEvent("", { detail: event })) + emitEvent(new CustomEvent("", { detail: event })); await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { @@ -2758,24 +2578,18 @@ describe("ClientWidgetApi", () => { }, } satisfies IMatrixApiError, }, - }) - }) - }) - }) + }); + }); + }); + }); it("updates theme", () => { - clientWidgetApi.updateTheme({ name: "dark" }) - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.ThemeChange, - { name: "dark" }, - ) - }) + clientWidgetApi.updateTheme({ name: "dark" }); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.ThemeChange, { name: "dark" }); + }); it("updates language", () => { - clientWidgetApi.updateLanguage("tlh") - expect(transport.send).toHaveBeenCalledWith( - WidgetApiToWidgetAction.LanguageChange, - { lang: "tlh" }, - ) - }) -}) + clientWidgetApi.updateLanguage("tlh"); + expect(transport.send).toHaveBeenCalledWith(WidgetApiToWidgetAction.LanguageChange, { lang: "tlh" }); + }); +}); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index da86955..e348a62 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -15,16 +15,16 @@ * limitations under the License. */ -import { UnstableApiVersion } from "../src/interfaces/ApiVersion" -import { IGetMediaConfigActionFromWidgetResponseData } from "../src/interfaces/GetMediaConfigAction" -import { IReadRelationsFromWidgetResponseData } from "../src/interfaces/ReadRelationsAction" -import { ISendEventFromWidgetResponseData } from "../src/interfaces/SendEventAction" -import { ISupportedVersionsActionResponseData } from "../src/interfaces/SupportedVersionsAction" -import { IUploadFileActionFromWidgetResponseData } from "../src/interfaces/UploadFileAction" -import { IDownloadFileActionFromWidgetResponseData } from "../src/interfaces/DownloadFileAction" -import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/UserDirectorySearchAction" -import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction" -import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi" +import { UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { IGetMediaConfigActionFromWidgetResponseData } from "../src/interfaces/GetMediaConfigAction"; +import { IReadRelationsFromWidgetResponseData } from "../src/interfaces/ReadRelationsAction"; +import { ISendEventFromWidgetResponseData } from "../src/interfaces/SendEventAction"; +import { ISupportedVersionsActionResponseData } from "../src/interfaces/SupportedVersionsAction"; +import { IUploadFileActionFromWidgetResponseData } from "../src/interfaces/UploadFileAction"; +import { IDownloadFileActionFromWidgetResponseData } from "../src/interfaces/DownloadFileAction"; +import { IUserDirectorySearchFromWidgetResponseData } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApi, WidgetApiResponseError } from "../src/WidgetApi"; import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails, @@ -34,83 +34,73 @@ import { IWidgetApiResponseData, UpdateDelayedEventAction, WidgetApiDirection, -} from "../src" +} from "../src"; type SendRequestArgs = { - action: WidgetApiFromWidgetAction - data: IWidgetApiRequestData -} + action: WidgetApiFromWidgetAction; + data: IWidgetApiRequestData; +}; class TransportChannels { /** Data sent by widget requests */ - public readonly requestQueue: Array = [] + public readonly requestQueue: Array = []; /** Responses to send as if from a client. Initialized with the response to {@link WidgetApi.start}*/ public readonly responseQueue: IWidgetApiResponseData[] = [ { supported_versions: [], } satisfies ISupportedVersionsActionResponseData, - ] + ]; } class WidgetTransportHelper { /** For ignoring the request sent by {@link WidgetApi.start} */ - private skippedFirstRequest = false + private skippedFirstRequest = false; public constructor(private channels: TransportChannels) {} public nextTrackedRequest(): SendRequestArgs | undefined { if (!this.skippedFirstRequest) { - this.skippedFirstRequest = true - this.channels.requestQueue.shift() + this.skippedFirstRequest = true; + this.channels.requestQueue.shift(); } - return this.channels.requestQueue.shift() + return this.channels.requestQueue.shift(); } public queueResponse(data: IWidgetApiResponseData): void { - this.channels.responseQueue.push(data) + this.channels.responseQueue.push(data); } } class ClientTransportHelper { public constructor(private channels: TransportChannels) {} - public trackRequest( - action: WidgetApiFromWidgetAction, - data: IWidgetApiRequestData, - ): void { - this.channels.requestQueue.push({ action, data }) + public trackRequest(action: WidgetApiFromWidgetAction, data: IWidgetApiRequestData): void { + this.channels.requestQueue.push({ action, data }); } public nextQueuedResponse(): IWidgetApiRequestData | undefined { - return this.channels.responseQueue.shift() + return this.channels.responseQueue.shift(); } } describe("WidgetApi", () => { - let widgetApi: WidgetApi - let widgetTransportHelper: WidgetTransportHelper - let clientListener: (e: MessageEvent) => void + let widgetApi: WidgetApi; + let widgetTransportHelper: WidgetTransportHelper; + let clientListener: (e: MessageEvent) => void; beforeEach(() => { - const channels = new TransportChannels() - widgetTransportHelper = new WidgetTransportHelper(channels) - const clientTrafficHelper = new ClientTransportHelper(channels) + const channels = new TransportChannels(); + widgetTransportHelper = new WidgetTransportHelper(channels); + const clientTrafficHelper = new ClientTransportHelper(channels); clientListener = (e: MessageEvent): void => { - if (!e.data.action || !e.data.requestId || !e.data.widgetId) return // invalid request/response - if ( - "response" in e.data || - e.data.api !== WidgetApiDirection.FromWidget - ) - return // not a request - const request = e.data - - clientTrafficHelper.trackRequest( - request.action as WidgetApiFromWidgetAction, - request.data, - ) - - const response = clientTrafficHelper.nextQueuedResponse() + if (!e.data.action || !e.data.requestId || !e.data.widgetId) return; // invalid request/response + if ("response" in e.data || e.data.api !== WidgetApiDirection.FromWidget) return; // not a request + const request = e.data; + + clientTrafficHelper.trackRequest(request.action as WidgetApiFromWidgetAction, request.data); + + const response = clientTrafficHelper.nextQueuedResponse(); if (response) { window.postMessage( { @@ -118,27 +108,27 @@ describe("WidgetApi", () => { response: response, } satisfies IWidgetApiResponse, "*", - ) + ); } - } - window.addEventListener("message", clientListener) + }; + window.addEventListener("message", clientListener); - widgetApi = new WidgetApi("WidgetApi-test", "*") - widgetApi.start() - }) + widgetApi = new WidgetApi("WidgetApi-test", "*"); + widgetApi.start(); + }); afterEach(() => { - window.removeEventListener("message", clientListener) - }) + window.removeEventListener("message", clientListener); + }); describe("readEventRelations", () => { it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ chunk: [], - } as IReadRelationsFromWidgetResponseData) + } as IReadRelationsFromWidgetResponseData); await expect( widgetApi.readEventRelations( @@ -153,11 +143,9 @@ describe("WidgetApi", () => { ), ).resolves.toEqual({ chunk: [], - }) + }); - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: { @@ -170,13 +158,13 @@ describe("WidgetApi", () => { to: "to-token", direction: "f", }, - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); await expect( widgetApi.readEventRelations( @@ -189,25 +177,23 @@ describe("WidgetApi", () => { "to-token", "f", ), - ).rejects.toThrow( - "The read_relations action is not supported by the client.", - ) + ).rejects.toThrow("The read_relations action is not supported by the client."); - const request = widgetTransportHelper.nextTrackedRequest() - expect(request).not.toBeUndefined() + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); expect(request).not.toEqual({ action: WidgetApiFromWidgetAction.MSC3869ReadRelations, data: expect.anything(), - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); await expect( widgetApi.readEventRelations( @@ -220,13 +206,13 @@ describe("WidgetApi", () => { "to-token", "f", ), - ).rejects.toThrow("An error occurred") - }) + ).rejects.toThrow("An error occurred"); + }); it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3869], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { @@ -238,14 +224,14 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); await expect( widgetApi.readEventRelations( @@ -258,50 +244,44 @@ describe("WidgetApi", () => { "to-token", "f", ), - ).rejects.toThrow( - new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ).rejects.toThrow(new WidgetApiResponseError("An error occurred", errorDetails)); + }); + }); describe("sendEvent", () => { it("sends message events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", event_id: "$event", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).resolves.toEqual({ room_id: "!room-id", event_id: "$event", - }) - }) + }); + }); it("sends state events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", event_id: "$event", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); - await expect( - widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id"), - ).resolves.toEqual({ + await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id")).resolves.toEqual({ room_id: "!room-id", event_id: "$event", - }) - }) + }); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).rejects.toThrow("An error occurred") - }) + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( + "An error occurred", + ); + }); it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { @@ -314,114 +294,81 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id"), - ).rejects.toThrow( + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id")).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("delayed sendEvent", () => { it("sends delayed message events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", delay_id: "id", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); - await expect( - widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000), - ).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 2000)).resolves.toEqual({ room_id: "!room-id", delay_id: "id", - }) - }) + }); + }); it("sends delayed state events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", delay_id: "id", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); - await expect( - widgetApi.sendStateEvent( - "m.room.topic", - "", - {}, - "!room-id", - 2000, - ), - ).resolves.toEqual({ + await expect(widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 2000)).resolves.toEqual({ room_id: "!room-id", delay_id: "id", - }) - }) + }); + }); it("sends delayed child action message events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", delay_id: "id", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).resolves.toEqual({ + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).resolves.toEqual({ room_id: "!room-id", delay_id: "id", - }) - }) + }); + }); it("sends delayed child action state events", async () => { widgetTransportHelper.queueResponse({ room_id: "!room-id", delay_id: "id", - } as ISendEventFromWidgetResponseData) + } as ISendEventFromWidgetResponseData); await expect( - widgetApi.sendStateEvent( - "m.room.topic", - "", - {}, - "!room-id", - 1000, - undefined, - ), + widgetApi.sendStateEvent("m.room.topic", "", {}, "!room-id", 1000, undefined), ).resolves.toEqual({ room_id: "!room-id", delay_id: "id", - }) - }) + }); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).rejects.toThrow("An error occurred") - }) + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( + "An error occurred", + ); + }); it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { @@ -434,52 +381,36 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.sendRoomEvent( - "m.room.message", - {}, - "!room-id", - 1000, - undefined, - ), - ).rejects.toThrow( + await expect(widgetApi.sendRoomEvent("m.room.message", {}, "!room-id", 1000, undefined)).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("updateDelayedEvent", () => { it("updates delayed events", async () => { - widgetTransportHelper.queueResponse({}) - await expect( - widgetApi.updateDelayedEvent( - "id", - UpdateDelayedEventAction.Send, - ), - ).resolves.toEqual({}) - }) + widgetTransportHelper.queueResponse({}); + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).resolves.toEqual({}); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.updateDelayedEvent( - "id", - UpdateDelayedEventAction.Send, - ), - ).rejects.toThrow("An error occurred") - }) + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( + "An error occurred", + ); + }); it("should handle an error with details", async () => { const errorDetails: IWidgetApiErrorResponseDataDetails = { @@ -492,126 +423,99 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.updateDelayedEvent( - "id", - UpdateDelayedEventAction.Send, - ), - ).rejects.toThrow( + await expect(widgetApi.updateDelayedEvent("id", UpdateDelayedEventAction.Send)).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("getClientVersions", () => { beforeEach(() => { widgetTransportHelper.queueResponse({ - supported_versions: [ - UnstableApiVersion.MSC3869, - UnstableApiVersion.MSC2762, - ], - } as ISupportedVersionsActionResponseData) - }) + supported_versions: [UnstableApiVersion.MSC3869, UnstableApiVersion.MSC2762], + } as ISupportedVersionsActionResponseData); + }); it("should request supported client versions", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]) - }) + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); + }); it("should cache supported client versions on successive calls", async () => { - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]) - - await expect(widgetApi.getClientVersions()).resolves.toEqual([ - "org.matrix.msc3869", - "org.matrix.msc2762", - ]) - - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() - expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined() - }) - }) + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); + + await expect(widgetApi.getClientVersions()).resolves.toEqual(["org.matrix.msc3869", "org.matrix.msc2762"]); + + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); + expect(widgetTransportHelper.nextTrackedRequest()).toBeUndefined(); + }); + }); describe("searchUserDirectory", () => { it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ limited: false, results: [], - } as IUserDirectorySearchFromWidgetResponseData) + } as IUserDirectorySearchFromWidgetResponseData); - await expect( - widgetApi.searchUserDirectory("foo", 10), - ).resolves.toEqual({ + await expect(widgetApi.searchUserDirectory("foo", 10)).resolves.toEqual({ limited: false, results: [], - }) + }); - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: { search_term: "foo", limit: 10, }, - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); - await expect( - widgetApi.searchUserDirectory("foo", 10), - ).rejects.toThrow( + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( "The user_directory_search action is not supported by the client.", - ) + ); - const request = widgetTransportHelper.nextTrackedRequest() - expect(request).not.toBeUndefined() + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); expect(request).not.toEqual({ action: WidgetApiFromWidgetAction.MSC3973UserDirectorySearch, data: expect.anything(), - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - }) + }); - await expect( - widgetApi.searchUserDirectory("foo", 10), - ).rejects.toThrow("An error occurred") - }) + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow("An error occurred"); + }); it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC3973], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { @@ -623,79 +527,73 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.searchUserDirectory("foo", 10), - ).rejects.toThrow( + await expect(widgetApi.searchUserDirectory("foo", 10)).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("getMediaConfig", () => { it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ "m.upload.size": 1000, - } as IGetMediaConfigActionFromWidgetResponseData) + } as IGetMediaConfigActionFromWidgetResponseData); await expect(widgetApi.getMediaConfig()).resolves.toEqual({ "m.upload.size": 1000, - }) + }); - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: {}, - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( "The get_media_config action is not supported by the client.", - ) + ); - const request = widgetTransportHelper.nextTrackedRequest() - expect(request).not.toBeUndefined() + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); expect(request).not.toEqual({ action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: expect.anything(), - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - }) + }); - await expect(widgetApi.getMediaConfig()).rejects.toThrow( - "An error occurred", - ) - }) + await expect(widgetApi.getMediaConfig()).rejects.toThrow("An error occurred"); + }); it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { @@ -707,77 +605,73 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); await expect(widgetApi.getMediaConfig()).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("uploadFile", () => { it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ content_uri: "mxc://...", - } as IUploadFileActionFromWidgetResponseData) + } as IUploadFileActionFromWidgetResponseData); await expect(widgetApi.uploadFile("data")).resolves.toEqual({ content_uri: "mxc://...", - }) + }); - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039UploadFileAction, data: { file: "data" }, - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); await expect(widgetApi.uploadFile("data")).rejects.toThrow( "The upload_file action is not supported by the client.", - ) + ); - const request = widgetTransportHelper.nextTrackedRequest() - expect(request).not.toBeUndefined() + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); expect(request).not.toEqual({ action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: expect.anything(), - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - }) + }); - await expect(widgetApi.uploadFile("data")).rejects.toThrow( - "An error occurred", - ) - }) + await expect(widgetApi.uploadFile("data")).rejects.toThrow("An error occurred"); + }); it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { @@ -789,81 +683,73 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); await expect(widgetApi.uploadFile("data")).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) + ); + }); + }); describe("downloadFile", () => { it("should forward the request to the ClientWidgetApi", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ file: "test contents", - } as IDownloadFileActionFromWidgetResponseData) + } as IDownloadFileActionFromWidgetResponseData); - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).resolves.toEqual({ + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).resolves.toEqual({ file: "test contents", - }) + }); - expect( - widgetTransportHelper.nextTrackedRequest(), - ).not.toBeUndefined() + expect(widgetTransportHelper.nextTrackedRequest()).not.toBeUndefined(); expect(widgetTransportHelper.nextTrackedRequest()).toEqual({ action: WidgetApiFromWidgetAction.MSC4039DownloadFileAction, data: { content_uri: "mxc://example.com/test_file" }, - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should reject the request if the api is not supported", async () => { widgetTransportHelper.queueResponse({ supported_versions: [], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow( + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( "The download_file action is not supported by the client.", - ) + ); - const request = widgetTransportHelper.nextTrackedRequest() - expect(request).not.toBeUndefined() + const request = widgetTransportHelper.nextTrackedRequest(); + expect(request).not.toBeUndefined(); expect(request).not.toEqual({ action: WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, data: expect.anything(), - } satisfies SendRequestArgs) - }) + } satisfies SendRequestArgs); + }); it("should handle an error", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); widgetTransportHelper.queueResponse({ error: { message: "An error occurred" }, - }) + }); - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow("An error occurred") - }) + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow("An error occurred"); + }); it("should handle an error with details", async () => { widgetTransportHelper.queueResponse({ supported_versions: [UnstableApiVersion.MSC4039], - } as ISupportedVersionsActionResponseData) + } as ISupportedVersionsActionResponseData); const errorDetails: IWidgetApiErrorResponseDataDetails = { matrix_api_error: { @@ -875,20 +761,18 @@ describe("WidgetApi", () => { error: "Unknown error", }, }, - } + }; widgetTransportHelper.queueResponse({ error: { message: "An error occurred", ...errorDetails, }, - } as IWidgetApiErrorResponseData) + } as IWidgetApiErrorResponseData); - await expect( - widgetApi.downloadFile("mxc://example.com/test_file"), - ).rejects.toThrow( + await expect(widgetApi.downloadFile("mxc://example.com/test_file")).rejects.toThrow( new WidgetApiResponseError("An error occurred", errorDetails), - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/test/url-template-test.ts b/test/url-template-test.ts index 549ad1d..3f28df8 100644 --- a/test/url-template-test.ts +++ b/test/url-template-test.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { runTemplate } from "../src" +import { runTemplate } from "../src"; describe("runTemplate", () => { it("should replace device id template in url", () => { - const url = - "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id" + const url = "https://localhost/?my-query#device_id=$org.matrix.msc3819.matrix_device_id"; const replacedUrl = runTemplate( url, { @@ -32,16 +31,13 @@ describe("runTemplate", () => { deviceId: "my-device-id", currentUserId: "@user-id", }, - ) + ); - expect(replacedUrl).toBe( - "https://localhost/?my-query#device_id=my-device-id", - ) - }) + expect(replacedUrl).toBe("https://localhost/?my-query#device_id=my-device-id"); + }); it("should replace base url template in url", () => { - const url = - "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url" + const url = "https://localhost/?my-query#base_url=$org.matrix.msc4039.matrix_base_url"; const replacedUrl = runTemplate( url, { @@ -54,10 +50,8 @@ describe("runTemplate", () => { currentUserId: "@user-id", baseUrl: "https://localhost/api", }, - ) + ); - expect(replacedUrl).toBe( - "https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi", - ) - }) -}) + expect(replacedUrl).toBe("https://localhost/?my-query#base_url=https%3A%2F%2Flocalhost%2Fapi"); + }); +});