From 9a2bf4980a3d5144365d0dde06935758960cd99e Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 14:37:32 +0100 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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"); + }); +}); From 3a5f87cce785daac4894f9be3ca96b8c474b5ea6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 27 Jan 2025 11:20:32 +0000 Subject: [PATCH 12/14] Remove vscode settings.json which only contains a personal preference --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d3def91..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.formatOnSave": false -} From 318dcc788e5ab271fa1a32eb317fc74b945befd1 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 27 Jan 2025 12:15:27 +0000 Subject: [PATCH 13/14] Smaller diff from code review --- src/ClientWidgetApi.ts | 44 +++-------- src/WidgetApi.ts | 15 +--- src/transport/PostmessageTransport.ts | 6 +- test/ClientWidgetApi-test.ts | 104 ++++++-------------------- test/WidgetApi-test.ts | 44 +++-------- 5 files changed, 48 insertions(+), 165 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 169f66d..9c949b4 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -469,9 +469,7 @@ 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" }, }); } @@ -502,9 +500,7 @@ export class ClientWidgetApi extends EventEmitter { 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}` }, }); } } @@ -519,18 +515,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" }, }); } } @@ -550,9 +542,7 @@ export class ClientWidgetApi extends EventEmitter { ), ) ).flat(1); - this.transport.reply(request, { - events, - }); + this.transport.reply(request, { events }); } private handleSendEvent(request: ISendEventFromWidgetActionRequest): void { @@ -564,9 +554,7 @@ export class ClientWidgetApi extends EventEmitter { 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}` }, }); } @@ -581,9 +569,7 @@ export class ClientWidgetApi extends EventEmitter { 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" }, }); } @@ -609,9 +595,7 @@ export class ClientWidgetApi extends EventEmitter { 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" }, }); } @@ -682,9 +666,7 @@ export class ClientWidgetApi extends EventEmitter { break; default: return this.transport.reply(request, { - error: { - message: "Invalid request - unsupported action", - }, + error: { message: "Invalid request - unsupported action" }, }); } } @@ -797,9 +779,7 @@ export class ClientWidgetApi extends EventEmitter { 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}`, - }, + error: { message: `Unable to access room timeline: ${request.data.room_id}` }, }); } @@ -1001,9 +981,7 @@ export class ClientWidgetApi extends EventEmitter { * @param lang The BCP 47 identifier representing the client's current language. */ public updateLanguage(lang: string): Promise { - return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { - lang, - }); + return this.transport.send(WidgetApiToWidgetAction.LanguageChange, { lang }); } /** diff --git a/src/WidgetApi.ts b/src/WidgetApi.ts index d81c8aa..44f0de9 100644 --- a/src/WidgetApi.ts +++ b/src/WidgetApi.ts @@ -468,9 +468,7 @@ export class WidgetApi extends EventEmitter { ...(stateKey !== undefined && { state_key: stateKey }), ...(roomId !== undefined && { room_id: roomId }), ...(delay !== undefined && { delay }), - ...(parentDelayId !== undefined && { - parent_delay_id: parentDelayId, - }), + ...(parentDelayId !== undefined && { parent_delay_id: parentDelayId }), }, ); } @@ -505,11 +503,7 @@ export class WidgetApi extends EventEmitter { ): Promise { return this.transport.send( WidgetApiFromWidgetAction.SendToDevice, - { - type: eventType, - encrypted, - messages: contentMap, - }, + { type: eventType, encrypted, messages: contentMap }, ); } @@ -538,10 +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; } diff --git a/src/transport/PostmessageTransport.ts b/src/transport/PostmessageTransport.ts index d6d502d..4589735 100644 --- a/src/transport/PostmessageTransport.ts +++ b/src/transport/PostmessageTransport.ts @@ -138,11 +138,7 @@ export class PostmessageTransport extends EventEmitter implements ITransport { this.stopController.signal.removeEventListener("abort", onStop); }; - this.outboundRequests.set(request.requestId, { - request, - resolve, - reject, - }); + this.outboundRequests.set(request.requestId, { request, resolve, reject }); this.sendInternal(request); }); } diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 2ca010a..a9ae879 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -151,6 +151,7 @@ describe("ClientWidgetApi", () => { iframe, driver, ); + [transport] = jest.mocked(PostmessageTransport).mock.instances; emitEvent = jest.mocked(transport.on).mock.calls[0][1]; @@ -692,11 +693,7 @@ 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", - }); + const event = createRoomEvent({ room_id: roomId, type: "m.room.message", content: "hello" }); const eventFromOtherRoom = createRoomEvent({ room_id: otherRoomId, type: "m.room.message", @@ -1140,9 +1137,7 @@ describe("ClientWidgetApi", () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Invalid request - missing event contents", - }, + error: { message: "Invalid request - missing event contents" }, }); }); @@ -1173,9 +1168,7 @@ describe("ClientWidgetApi", () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Invalid request - missing encryption flag", - }, + error: { message: "Invalid request - missing encryption flag" }, }); }); @@ -1207,9 +1200,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); @@ -1355,9 +1346,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); @@ -1437,9 +1426,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); @@ -1450,11 +1437,7 @@ 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", - }); + const event = createRoomEvent({ room_id: roomId, type: "net.example.test", content: "test" }); driver.readRoomTimeline.mockImplementation(async (rId) => { if (rId === roomId) return [event]; return []; @@ -1498,16 +1481,8 @@ 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", - }); + 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]; @@ -1583,14 +1558,8 @@ describe("ClientWidgetApi", () => { 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" }), ], }); }); @@ -1651,12 +1620,7 @@ 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" })], }); }); @@ -1754,14 +1718,8 @@ describe("ClientWidgetApi", () => { chunk: [ createRoomEvent(), createRoomEvent({ type: "m.reaction" }), - 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" }), ], }); @@ -1782,13 +1740,7 @@ 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" })], }); }); @@ -1898,9 +1850,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); @@ -1923,9 +1873,7 @@ describe("ClientWidgetApi", () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while reading relations", - }, + error: { message: "Unexpected error while reading relations" }, }); }); }); @@ -2183,9 +2131,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); }); @@ -2311,9 +2257,7 @@ describe("ClientWidgetApi", () => { 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" }, }); }); }); @@ -2447,9 +2391,7 @@ describe("ClientWidgetApi", () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while uploading a file", - }, + error: { message: "Unexpected error while uploading a file" }, }); }); }); @@ -2567,9 +2509,7 @@ describe("ClientWidgetApi", () => { await waitFor(() => { expect(transport.reply).toBeCalledWith(event, { - error: { - message: "Unexpected error while downloading a file", - }, + error: { message: "Unexpected error while downloading a file" }, }); }); }); diff --git a/test/WidgetApi-test.ts b/test/WidgetApi-test.ts index e348a62..b128e1c 100644 --- a/test/WidgetApi-test.ts +++ b/test/WidgetApi-test.ts @@ -46,9 +46,7 @@ class TransportChannels { 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, + { supported_versions: [] } satisfies ISupportedVersionsActionResponseData, ]; } @@ -162,9 +160,7 @@ 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.readEventRelations( @@ -485,9 +481,7 @@ 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.", @@ -505,9 +499,7 @@ describe("WidgetApi", () => { 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"); }); @@ -563,9 +555,7 @@ 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.", @@ -583,9 +573,7 @@ describe("WidgetApi", () => { 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"); }); @@ -641,9 +629,7 @@ 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.uploadFile("data")).rejects.toThrow( "The upload_file action is not supported by the client.", @@ -661,9 +647,7 @@ describe("WidgetApi", () => { 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"); }); @@ -703,9 +687,7 @@ describe("WidgetApi", () => { 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", @@ -719,9 +701,7 @@ 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.downloadFile("mxc://example.com/test_file")).rejects.toThrow( "The download_file action is not supported by the client.", @@ -739,9 +719,7 @@ describe("WidgetApi", () => { 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"); }); From 697ea196886f3cf972f6c2adfcf2e5ea6936f280 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 27 Jan 2025 11:47:06 +0000 Subject: [PATCH 14/14] Define widget driver as an interface - Adds `IWidgetDriver` interface - Renames `WidgetDriver` to `BaseWidgetDriver` with old name (`WidgetDriver`) supported but deprecated The goal here is to mean that a client that is implementing a driver can more easily see at compile time what is changing between versions. However, this would mean that we need to change the versioning policy as any addition to IWidgetDriver would be a breaking change (so major version bump). Whereas, at the moment we don't do a major version bump because the abstract WidgetDriver class doesn't require the implementing client to implement anything. --- README.md | 2 +- src/ClientWidgetApi.ts | 6 +- src/driver/WidgetDriver.ts | 254 ++++++++++++++++++++++++----------- test/ClientWidgetApi-test.ts | 6 +- 4 files changed, 182 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index a29fac5..d8c5345 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ SDK to provide an interface for other platforms. TODO: Improve this ```typescript -const driver = new CustomDriver(); // an implementation of WidgetDriver +const driver = new CustomDriver(); // an implementation of IWidgetDriver const api = new ClientWidgetApi(widget, iframe, driver); // The API is automatically started, so we just have to wait for a ready before doing something diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 9c949b4..705f3a2 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -30,7 +30,7 @@ import { getTimelineRoomIDFromCapability, isTimelineCapability, } from "./interfaces/Capabilities"; -import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, WidgetDriver } from "./driver/WidgetDriver"; +import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, IWidgetDriver } from "./driver/WidgetDriver"; import { ICapabilitiesActionResponseData, INotifyCapabilitiesActionRequestData, @@ -158,12 +158,12 @@ export class ClientWidgetApi extends EventEmitter { * 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. + * @param {IWidgetDriver} driver The driver for this widget/client. */ public constructor( public readonly widget: Widget, private iframe: HTMLIFrameElement, - private driver: WidgetDriver, + private driver: IWidgetDriver, ) { super(); if (!iframe?.contentWindow) { diff --git a/src/driver/WidgetDriver.ts b/src/driver/WidgetDriver.ts index df92c03..97e19ad 100644 --- a/src/driver/WidgetDriver.ts +++ b/src/driver/WidgetDriver.ts @@ -63,14 +63,11 @@ export interface IGetMediaConfigResult { /** * Represents the functions and behaviour the widget-api is unable to - * do, such as prompting the user for information or interacting with - * the UI. Clients are expected to implement this class and override - * any functions they need/want to support. + * do such as prompting the user for information or interacting with + * the UI. It must be implemented by the client that is embedding widgets. * - * This class assumes the client will have a context of a Widget - * instance already. */ -export abstract class WidgetDriver { +export interface IWidgetDriver { /** * Verifies the widget's requested capabilities, returning the ones * it is approved to use. Mutating the requested capabilities will @@ -83,9 +80,7 @@ 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()); - } + validateCapabilities(requested: Set): Promise>; /** * Sends an event into a room. If `roomId` is falsy, the client should send the event @@ -101,14 +96,12 @@ export abstract class WidgetDriver { * details of that event. * @throws Rejected when the event could not be sent. */ - public sendEvent( + sendEvent( eventType: string, content: unknown, - stateKey: string | null = null, - roomId: string | null = null, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + stateKey?: string | null, + roomId?: string | null, + ): Promise; /** * @experimental Part of MSC4140 & MSC4157 @@ -129,25 +122,21 @@ export abstract class WidgetDriver { * 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( + 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")); - } + stateKey?: string | null, + roomId?: string | null, + ): Promise; /** * @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")); - } + updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise; /** * Sends a to-device event. The widget API will have already verified that the widget @@ -158,13 +147,12 @@ export abstract class WidgetDriver { * @returns {Promise} Resolves when the event has been sent. * @throws Rejected when the event could not be sent. */ - public sendToDevice( + sendToDevice( eventType: string, encrypted: boolean, contentMap: { [userId: string]: { [deviceId: string]: object } }, - ): Promise { - return Promise.reject(new Error("Failed to override function")); - } + ): Promise; + /** * 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 @@ -175,9 +163,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 { - return Promise.resolve([]); - } + readRoomAccountData(eventType: string, roomIds?: string[] | null): Promise; /** * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), @@ -201,15 +187,13 @@ export abstract class WidgetDriver { * @returns {Promise} Resolves to the room events, or an empty array. * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. */ - public readRoomEvents( + readRoomEvents( eventType: string, msgtype: string | undefined, limit: number, - roomIds: string[] | null = null, + roomIds?: string[] | null, since?: string, - ): Promise { - return Promise.resolve([]); - } + ): Promise; /** * Reads all events of the given type, and optionally state key (if applicable/defined), @@ -227,14 +211,12 @@ export abstract class WidgetDriver { * @returns {Promise} Resolves to the state events, or an empty array. * @deprecated Clients are advised to implement {@link WidgetDriver.readRoomTimeline} instead. */ - public readStateEvents( + readStateEvents( eventType: string, stateKey: string | undefined, limit: number, - roomIds: string[] | null = null, - ): Promise { - return Promise.resolve([]); - } + roomIds?: string[] | null, + ): Promise; /** * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), @@ -252,19 +234,14 @@ export abstract class WidgetDriver { * in "limit". * @returns {Promise} Resolves to the room events, or an empty array. */ - public readRoomTimeline( + 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]); - } + ): Promise; /** * Reads the current values of all matching room state entries. @@ -275,9 +252,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 { - return Promise.resolve([]); - } + readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise; /** * Reads all events that are related to a given event. The widget API will @@ -303,7 +278,7 @@ export abstract class WidgetDriver { * @param direction The direction to search for according to MSC3715 * @returns Resolves to the room relations. */ - public readEventRelations( + readEventRelations( eventId: string, roomId?: string, relationType?: string, @@ -312,9 +287,7 @@ export abstract class WidgetDriver { to?: string, limit?: number, direction?: "f" | "b", - ): Promise { - return Promise.resolve({ chunk: [] }); - } + ): Promise; /** * Asks the user for permission to validate their identity through OpenID Connect. The @@ -329,9 +302,7 @@ export abstract class WidgetDriver { * 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 }); - } + askOpenID(observer: SimpleObservable): void; /** * Navigates the client with a matrix.to URI. In future this function will also be provided @@ -342,9 +313,7 @@ export abstract class WidgetDriver { * @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"); - } + navigate(uri: string): Promise; /** * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and @@ -352,9 +321,7 @@ export abstract class WidgetDriver { * 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"); - } + getTurnServers(): AsyncGenerator; /** * Search for users in the user directory. @@ -362,17 +329,13 @@ 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: [] }); - } + searchUserDirectory(searchTerm: string, limit?: number): Promise; /** * 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"); - } + getMediaConfig(): Promise; /** * Upload a file to the media repository on the homeserver. @@ -380,27 +343,21 @@ 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"); - } + uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }>; /** * 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"); - } + downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }>; /** * 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"); - } + getKnownRooms(): string[]; /** * Expresses an error thrown by this driver in a format compatible with the Widget API. @@ -408,7 +365,146 @@ export abstract class WidgetDriver { * @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails}, * or undefined if it cannot be expressed as one. */ + processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined; +} + +/** + * Provides a convenience base class to implement the IWidgetDriver interface + * Clients can extend and override this class for any functions they need/want + * to support. + * + * This class assumes the client will have a context of a Widget + * instance already. + */ +export abstract class BaseWidgetDriver implements IWidgetDriver { + public validateCapabilities(requested: Set): Promise> { + return Promise.resolve(new Set()); + } + + public sendEvent( + eventType: string, + content: unknown, + stateKey: string | null = null, + roomId: string | null = null, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } + + 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")); + } + + public updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise { + return Promise.reject(new Error("Failed to override function")); + } + + public sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + return Promise.reject(new Error("Failed to override function")); + } + + public readRoomAccountData(eventType: string, roomIds: string[] | null = null): Promise { + return Promise.resolve([]); + } + + public readRoomEvents( + eventType: string, + msgtype: string | undefined, + limit: number, + roomIds: string[] | null = null, + since?: string, + ): Promise { + return Promise.resolve([]); + } + + public readStateEvents( + eventType: string, + stateKey: string | undefined, + limit: number, + roomIds: string[] | null = null, + ): Promise { + return Promise.resolve([]); + } + + 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]); + } + + public readRoomState(roomId: string, eventType: string, stateKey: string | undefined): Promise { + return Promise.resolve([]); + } + + public readEventRelations( + eventId: string, + roomId?: string, + relationType?: string, + eventType?: string, + from?: string, + to?: string, + limit?: number, + direction?: "f" | "b", + ): Promise { + return Promise.resolve({ chunk: [] }); + } + + public askOpenID(observer: SimpleObservable): void { + observer.update({ state: OpenIDRequestState.Blocked }); + } + + public navigate(uri: string): Promise { + throw new Error("Navigation is not implemented"); + } + + public getTurnServers(): AsyncGenerator { + throw new Error("TURN server support is not implemented"); + } + + public searchUserDirectory(searchTerm: string, limit?: number): Promise { + return Promise.resolve({ limited: false, results: [] }); + } + + public getMediaConfig(): Promise { + throw new Error("Get media config is not implemented"); + } + + public uploadFile(file: XMLHttpRequestBodyInit): Promise<{ contentUri: string }> { + throw new Error("Upload file is not implemented"); + } + + public downloadFile(contentUri: string): Promise<{ file: XMLHttpRequestBodyInit }> { + throw new Error("Download file is not implemented"); + } + + public getKnownRooms(): string[] { + throw new Error("Querying known rooms is not implemented"); + } + public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined { return undefined; } } + +/** + * @deprecated Use {@link BaseWidgetDriver} instead. + */ +export type WidgetDriver = BaseWidgetDriver; diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index a9ae879..dae5688 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -18,7 +18,7 @@ import { waitFor } from "@testing-library/dom"; import { ClientWidgetApi } from "../src/ClientWidgetApi"; -import { WidgetDriver } from "../src/driver/WidgetDriver"; +import { IWidgetDriver } from "../src/driver/WidgetDriver"; import { CurrentApiVersions, UnstableApiVersion } from "../src/interfaces/ApiVersion"; import { Capability } from "../src/interfaces/Capabilities"; import { IRoomEvent } from "../src/interfaces/IRoomEvent"; @@ -99,7 +99,7 @@ function processCustomMatrixError(e: unknown): IWidgetApiErrorResponseDataDetail describe("ClientWidgetApi", () => { let capabilities: Capability[]; let iframe: HTMLIFrameElement; - let driver: jest.Mocked; + let driver: jest.Mocked; let clientWidgetApi: ClientWidgetApi; let transport: PostmessageTransport; let emitEvent: Parameters["1"]; @@ -139,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({