diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 07ff3c7178..f19fa0c162 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -273,17 +273,9 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - if (proxyUrl) { - logger.log("Activating sliding sync using proxy at ", proxyUrl); - } else { - logger.log("Activating sliding sync"); - } - opts.slidingSync = SlidingSyncManager.instance.configure( - this.matrixClient, - proxyUrl || this.matrixClient.baseUrl, - ); - SlidingSyncManager.instance.startSpidering(100, 50); // 100 rooms at a time, 50ms apart + opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); + } else { + SlidingSyncManager.instance.checkSupport(this.matrixClient); } // Connect the matrix client to the dispatcher and setting handlers diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index 5f459c0b9e..e3f420b43d 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -44,7 +44,7 @@ limitations under the License. * list ops) */ -import { MatrixClient, EventType } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; import { MSC3575Filter, MSC3575List, @@ -56,6 +56,9 @@ import { import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; +import SettingsStore from "./settings/SettingsStore"; +import SlidingSyncController from "./settings/controllers/SlidingSyncController"; + // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; @@ -323,4 +326,93 @@ export class SlidingSyncManager { firstTime = false; } } + + /** + * Set up the Sliding Sync instance; configures the end point and starts spidering. + * The sliding sync endpoint is derived the following way: + * 1. The user-defined sliding sync proxy URL (legacy, for backwards compatibility) + * 2. The client `well-known` sliding sync proxy URL [declared at the unstable prefix](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#unstable-prefix) + * 3. The homeserver base url (for native server support) + * @param client The MatrixClient to use + * @returns A working Sliding Sync or undefined + */ + public async setup(client: MatrixClient): Promise { + const baseUrl = client.baseUrl; + const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); + const wellKnownProxyUrl = await this.getProxyFromWellKnown(client); + + const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl; + + this.configure(client, slidingSyncEndpoint); + logger.info("Sliding sync activated at", slidingSyncEndpoint); + this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart + + return this.slidingSync; + } + + /** + * Get the sliding sync proxy URL from the client well known + * @param client The MatrixClient to use + * @return The proxy url + */ + public async getProxyFromWellKnown(client: MatrixClient): Promise { + let proxyUrl: string | undefined; + + try { + const clientWellKnown = await AutoDiscovery.findClientConfig(client.baseUrl); + proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; + } catch (e) { + // client.baseUrl is invalid, `AutoDiscovery.findClientConfig` has thrown + } + + if (proxyUrl != undefined) { + logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); + } + return proxyUrl; + } + + /** + * Check if the server "natively" supports sliding sync (at the unstable endpoint). + * @param client The MatrixClient to use + * @return Whether the "native" (unstable) endpoint is up + */ + public async nativeSlidingSyncSupport(client: MatrixClient): Promise { + try { + await client.http.authedRequest(Method.Post, "/sync", undefined, undefined, { + localTimeoutMs: 10 * 1000, // 10s + prefix: "/_matrix/client/unstable/org.matrix.msc3575", + }); + } catch (e) { + return false; // 404, M_UNRECOGNIZED + } + + logger.log("nativeSlidingSyncSupport: sliding sync endpoint is up"); + return true; // 200, OK + } + + /** + * Check whether our homeserver has sliding sync support, that the endpoint is up, and + * is a sliding sync endpoint. + * + * Sets static member `SlidingSyncController.serverSupportsSlidingSync` + * @param client The MatrixClient to use + */ + public async checkSupport(client: MatrixClient): Promise { + if (await this.nativeSlidingSyncSupport(client)) { + SlidingSyncController.serverSupportsSlidingSync = true; + return; + } + + const proxyUrl = await this.getProxyFromWellKnown(client); + if (proxyUrl != undefined) { + const response = await fetch(proxyUrl + "/client/server.json", { + method: Method.Get, + signal: timeoutSignal(10 * 1000), // 10s + }); + if (response.status === 200) { + logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); + SlidingSyncController.serverSupportsSlidingSync = true; + } + } + } } diff --git a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx b/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx deleted file mode 100644 index 958c8d0876..0000000000 --- a/src/components/views/dialogs/SlidingSyncOptionsDialog.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { MatrixClient, Method } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { _t } from "../../../languageHandler"; -import SettingsStore from "../../../settings/SettingsStore"; -import TextInputDialog from "./TextInputDialog"; -import withValidation from "../elements/Validation"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; -import { SettingLevel } from "../../../settings/SettingLevel"; - -/** - * Check that the server natively supports sliding sync. - * @param cli The MatrixClient of the logged in user. - * @throws if the proxy server is unreachable or not configured to the given homeserver - */ -async function syncHealthCheck(cli: MatrixClient): Promise { - await cli.http.authedRequest(Method.Post, "/sync", undefined, undefined, { - localTimeoutMs: 10 * 1000, // 10s - prefix: "/_matrix/client/unstable/org.matrix.msc3575", - }); - logger.info("server natively support sliding sync OK"); -} - -/** - * Check that the proxy url is in fact a sliding sync proxy endpoint and it is up. - * @param endpoint The proxy endpoint url - * @param hsUrl The homeserver url of the logged in user. - * @throws if the proxy server is unreachable or not configured to the given homeserver - */ -async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise { - const controller = new AbortController(); - const id = window.setTimeout(() => controller.abort(), 10 * 1000); // 10s - const res = await fetch(endpoint + "/client/server.json", { - signal: controller.signal, - }); - clearTimeout(id); - if (res.status != 200) { - throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`); - } - const body = await res.json(); - if (body.server !== hsUrl) { - throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`); - } - logger.info("sliding sync proxy is OK"); -} - -export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean): void }> = ({ onFinished }) => { - const cli = MatrixClientPeg.safeGet(); - const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const hasNativeSupport = useAsyncMemo( - () => - syncHealthCheck(cli).then( - () => true, - () => false, - ), - [], - null, - ); - - let nativeSupport: string; - if (hasNativeSupport === null) { - nativeSupport = _t("labs|sliding_sync_checking"); - } else { - nativeSupport = hasNativeSupport - ? _t("labs|sliding_sync_server_support") - : _t("labs|sliding_sync_server_no_support"); - } - - const validProxy = withValidation({ - async deriveData({ value }): Promise<{ error?: unknown }> { - if (!value) return {}; - try { - await proxyHealthCheck(value, MatrixClientPeg.safeGet().baseUrl); - return {}; - } catch (error) { - return { error }; - } - }, - rules: [ - { - key: "required", - test: async ({ value }) => !!value || !!hasNativeSupport, - invalid: () => _t("labs|sliding_sync_server_specify_proxy"), - }, - { - key: "working", - final: true, - test: async (_, { error }) => !error, - valid: () => _t("spotlight|public_rooms|network_dropdown_available_valid"), - invalid: ({ error }) => (error instanceof Error ? error.message : null), - }, - ], - }); - - return ( - -
- {_t("labs|sliding_sync_disable_warning")} -
- {nativeSupport} - - } - placeholder={ - hasNativeSupport - ? _t("labs|sliding_sync_proxy_url_optional_label") - : _t("labs|sliding_sync_proxy_url_label") - } - value={currentProxy} - button={_t("action|enable")} - validator={validProxy} - onFinished={(enable, proxyUrl) => { - if (enable) { - SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl); - onFinished(true); - } else { - onFinished(false); - } - }} - /> - ); -}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ddd6e3e09c..d1f4016aaf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1460,16 +1460,9 @@ "rust_crypto_optin_warning": "Switching to the Rust cryptography requires a migration process that may take several minutes. To disable you will need to log out and back in; use with caution!", "rust_crypto_requires_logout": "Once enabled, Rust cryptography can only be disabled by logging out and in again", "sliding_sync": "Sliding Sync mode", - "sliding_sync_checking": "Checking…", - "sliding_sync_configuration": "Sliding Sync configuration", "sliding_sync_description": "Under active development, cannot be disabled.", - "sliding_sync_disable_warning": "To disable you will need to log out and back in, use with caution!", "sliding_sync_disabled_notice": "Log out and back in to disable", - "sliding_sync_proxy_url_label": "Proxy URL", - "sliding_sync_proxy_url_optional_label": "Proxy URL (optional)", - "sliding_sync_server_no_support": "Your server lacks native support", - "sliding_sync_server_specify_proxy": "Your server lacks native support, you must specify a proxy", - "sliding_sync_server_support": "Your server has native support", + "sliding_sync_server_no_support": "Your server lacks support", "under_active_development": "Under active development.", "unrealiable_e2e": "Unreliable in encrypted rooms", "video_rooms": "Video rooms", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 2137837500..3650e51814 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -407,7 +407,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { controller: new SlidingSyncController(), }, "feature_sliding_sync_proxy_url": { - // This is not a distinct feature, it is a setting for feature_sliding_sync above + // This is not a distinct feature, it is a legacy setting for feature_sliding_sync above supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: "", }, diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 77bdf7f42f..7d7ca78128 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -1,5 +1,6 @@ /* Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2024 Ed Geraghty Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,18 +17,11 @@ limitations under the License. import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; -import { SettingLevel } from "../SettingLevel"; -import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog"; -import Modal from "../../Modal"; import SettingsStore from "../SettingsStore"; import { _t } from "../../languageHandler"; export default class SlidingSyncController extends SettingController { - public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise { - const { finished } = Modal.createDialog(SlidingSyncOptionsDialog); - const [value] = await finished; - return newValue === value; // abort the operation if we're already in the state the user chose via modal - } + public static serverSupportsSlidingSync: boolean; public async onChange(): Promise { PlatformPeg.get()?.reload(); @@ -38,6 +32,9 @@ export default class SlidingSyncController extends SettingController { if (SettingsStore.getValue("feature_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } + if (!SlidingSyncController.serverSupportsSlidingSync) { + return _t("labs|sliding_sync_server_no_support"); + } return false; } diff --git a/test/SlidingSyncManager-test.ts b/test/SlidingSyncManager-test.ts index 76ebd8f15c..757a682d84 100644 --- a/test/SlidingSyncManager-test.ts +++ b/test/SlidingSyncManager-test.ts @@ -20,6 +20,8 @@ import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { SlidingSyncManager } from "../src/SlidingSyncManager"; import { stubClient } from "./test-utils"; +import SlidingSyncController from "../src/settings/controllers/SlidingSyncController"; +import SettingsStore from "../src/settings/SettingsStore"; jest.mock("matrix-js-sdk/src/sliding-sync"); const MockSlidingSync = >(SlidingSync); @@ -231,4 +233,53 @@ describe("SlidingSyncManager", () => { ); }); }); + describe("checkSupport", () => { + beforeEach(() => { + SlidingSyncController.serverSupportsSlidingSync = false; + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + }); + it("shorts out if the server has 'native' sliding sync support", async () => { + jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); + expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier + expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + }); + it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => { + jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + await manager.checkSupport(client); + expect(manager.getProxyFromWellKnown).toHaveBeenCalled(); + expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + }); + }); + describe("setup", () => { + beforeEach(() => { + jest.spyOn(manager, "configure"); + jest.spyOn(manager, "startSpidering"); + }); + it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => { + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + it("uses the proxy declared in the client well-known", async () => { + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, "proxy"); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { + jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("proxy"); + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy"; + }); + await manager.setup(client); + expect(manager.configure).toHaveBeenCalled(); + expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy"); + expect(manager.startSpidering).toHaveBeenCalled(); + }); + }); });