mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Config.init() derives the location of config.json from window.location,
which only makes sense while Element Call owns the page. When it is
embedded in a host application the host owns the configuration, so add
Config.initWith() to accept it directly.
Share the defaulting and validation between both paths via resolveConfig(),
so that an injected config behaves identically to a fetched one, and mark
initialization as complete so that the init() calls already on the startup
path resolve immediately instead of fetching over the top of it.
initDefault() becomes initWith({}), which also stops it handing out a
shallow copy of DEFAULT_CONFIG whose nested objects were shared with the
module-level default.
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
/*
|
|
Copyright 2021-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { merge } from "lodash-es";
|
|
import { logger } from "matrix-js-sdk/lib/logger";
|
|
|
|
import { getUrlParams } from "../UrlParams";
|
|
import {
|
|
DEFAULT_CONFIG,
|
|
type ConfigOptions,
|
|
type ResolvedConfigOptions,
|
|
} from "./ConfigOptions";
|
|
import { isFailure } from "../utils/fetch";
|
|
import { MatrixRTCMode } from "./ConfigOptions";
|
|
|
|
const VALID_MATRIX_RTC_MODES: ReadonlySet<string> = new Set(
|
|
Object.values(MatrixRTCMode),
|
|
);
|
|
|
|
export class Config {
|
|
private static internalInstance: Config | undefined;
|
|
|
|
public static get(): ResolvedConfigOptions {
|
|
if (!this.internalInstance?.config)
|
|
throw new Error("Config instance read before config got initialized");
|
|
return this.internalInstance.config;
|
|
}
|
|
|
|
/**
|
|
* Initializes the config by fetching `config.json`, locating it relative to
|
|
* the current page.
|
|
*
|
|
* Does nothing if the config has already been initialized, including by
|
|
* {@link Config.initWith}, so that the regular startup path can run unchanged
|
|
* when an embedder has already supplied the config.
|
|
*/
|
|
public static async init(): Promise<void> {
|
|
if (!Config.internalInstance?.initPromise) {
|
|
const internalInstance = new Config();
|
|
Config.internalInstance = internalInstance;
|
|
|
|
let fetchTarget: string;
|
|
|
|
if (
|
|
window.location.pathname.endsWith("/room/") ||
|
|
window.location.pathname.endsWith("/room")
|
|
) {
|
|
// it looks like we are running in standalone mode so use the config at the root
|
|
fetchTarget = new URL("/config.json", window.location.href).href;
|
|
} else {
|
|
// otherwise we are probably running as a widget so use the config in the same directory
|
|
fetchTarget = "config.json";
|
|
}
|
|
|
|
Config.internalInstance.initPromise = downloadConfig(fetchTarget).then(
|
|
(config) => {
|
|
internalInstance.config = resolveConfig(config);
|
|
},
|
|
);
|
|
}
|
|
return Config.internalInstance.initPromise;
|
|
}
|
|
|
|
/**
|
|
* Initializes the config from an object supplied by the embedder, instead of
|
|
* fetching `config.json`.
|
|
*
|
|
* {@link Config.init} derives the location of `config.json` from
|
|
* `window.location`, which only makes sense while Element Call owns the page.
|
|
* When it is embedded in a host application the host owns the configuration
|
|
* and passes it in here.
|
|
*
|
|
* The config goes through the same validation and defaulting as a fetched
|
|
* one, so that an injected config behaves identically to a hosted one.
|
|
*
|
|
* Replaces any config initialized earlier.
|
|
*/
|
|
public static initWith(config: ConfigOptions): void {
|
|
const internalInstance = new Config();
|
|
internalInstance.config = resolveConfig(config);
|
|
// Mark initialization as already done, so that a later init() resolves
|
|
// immediately rather than fetching config.json over the top of this.
|
|
internalInstance.initPromise = Promise.resolve();
|
|
Config.internalInstance = internalInstance;
|
|
}
|
|
|
|
/**
|
|
* This is a alternative initializer that does not load anything
|
|
* from a hosted config file but instead just initializes the config using the
|
|
* default config.
|
|
*
|
|
* It is supposed to only be used in tests. (It is executed in `vite.setup.js`)
|
|
*/
|
|
public static initDefault(): void {
|
|
Config.initWith({});
|
|
}
|
|
|
|
// Convenience accessors
|
|
public static defaultHomeserverUrl(): string | undefined {
|
|
return (
|
|
getUrlParams().homeserver ??
|
|
Config.get().default_server_config?.["m.homeserver"].base_url
|
|
);
|
|
}
|
|
|
|
public static defaultServerName(): string | undefined {
|
|
const homeserver = getUrlParams().homeserver;
|
|
if (homeserver) {
|
|
const url = new URL(homeserver);
|
|
return url.hostname;
|
|
}
|
|
return Config.get().default_server_config?.["m.homeserver"].server_name;
|
|
}
|
|
|
|
public config?: ResolvedConfigOptions;
|
|
private initPromise?: Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Applies validation and the built-in defaults to a config, however it was
|
|
* obtained. Deep-merges onto a fresh object so that the result never shares
|
|
* nested state with {@link DEFAULT_CONFIG}.
|
|
*/
|
|
function resolveConfig(config: ConfigOptions): ResolvedConfigOptions {
|
|
return merge({}, DEFAULT_CONFIG, validateConfig(config));
|
|
}
|
|
|
|
export function validateConfig(config: ConfigOptions): ConfigOptions {
|
|
const mode = config.matrix_rtc_mode;
|
|
if (mode !== undefined && !VALID_MATRIX_RTC_MODES.has(mode)) {
|
|
logger.warn(
|
|
`Ignoring invalid matrix_rtc_mode in config.json: ${String(mode)}`,
|
|
);
|
|
delete config.matrix_rtc_mode;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
async function downloadConfig(fetchTarget: string): Promise<ConfigOptions> {
|
|
const response = await fetch(fetchTarget);
|
|
|
|
if (isFailure(response)) {
|
|
// Lack of a config isn't an error, we should just use the defaults.
|
|
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
|
|
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
|
|
return DEFAULT_CONFIG;
|
|
}
|
|
|
|
return response.json();
|
|
}
|