Files
element-call/src/config/Config.ts
T

154 lines
5.1 KiB
TypeScript
Raw Normal View History

2022-11-03 19:43:41 +01:00
/*
Copyright 2021-2024 New Vector Ltd.
2022-11-03 19:43:41 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
2022-11-03 19:43:41 +01:00
*/
2024-11-14 12:18:43 +01:00
import { merge } from "lodash-es";
2026-06-05 11:49:43 +02:00
import { logger } from "matrix-js-sdk/lib/logger";
2024-02-21 21:52:31 +01:00
import { getUrlParams } from "../UrlParams";
2022-12-21 15:25:08 +00:00
import {
DEFAULT_CONFIG,
type ConfigOptions,
type ResolvedConfigOptions,
2022-12-21 15:25:08 +00:00
} from "./ConfigOptions";
import { isFailure } from "../utils/fetch";
2026-06-05 11:49:43 +02:00
import { MatrixRTCMode } from "./ConfigOptions";
const VALID_MATRIX_RTC_MODES: ReadonlySet<string> = new Set(
Object.values(MatrixRTCMode),
);
2022-11-03 19:43:41 +01:00
export class Config {
private static internalInstance: Config | undefined;
2022-12-21 10:17:53 +00:00
public static get(): ResolvedConfigOptions {
2022-12-21 10:17:53 +00:00
if (!this.internalInstance?.config)
2022-11-03 19:43:41 +01:00
throw new Error("Config instance read before config got initialized");
2022-12-21 10:17:53 +00:00
return this.internalInstance.config;
2022-11-03 19:43:41 +01:00
}
2022-12-21 10:17:53 +00:00
/**
* 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
2026-09-08 15:09:11 +02:00
* when a component host has already supplied the config.
*/
public static async init(): Promise<void> {
if (!Config.internalInstance?.initPromise) {
const internalInstance = new Config();
Config.internalInstance = internalInstance;
2025-03-12 17:00:44 +00:00
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);
},
);
}
2022-11-03 19:43:41 +01:00
return Config.internalInstance.initPromise;
}
/**
2026-09-08 15:09:11 +02:00
* Initializes the config from an object supplied by the application hosting
* the component, 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.
2026-09-08 15:09:11 +02:00
* As a component, the host owns the configuration and passes it in here.
*
* The config goes through the same validation and defaulting as a fetched
2026-09-08 15:09:11 +02:00
* one, so that a supplied config behaves identically to a fetched 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({});
}
2022-12-20 17:26:45 +00:00
// Convenience accessors
public static defaultHomeserverUrl(): string | undefined {
2024-02-21 21:52:31 +01:00
return (
getUrlParams().homeserver ??
Config.get().default_server_config?.["m.homeserver"].base_url
);
2022-12-20 17:26:45 +00:00
}
public static defaultServerName(): string | undefined {
2024-02-21 21:52:31 +01:00
const homeserver = getUrlParams().homeserver;
if (homeserver) {
const url = new URL(homeserver);
return url.hostname;
}
2023-06-30 16:43:28 +01:00
return Config.get().default_server_config?.["m.homeserver"].server_name;
2022-12-20 17:26:45 +00:00
}
2022-12-21 15:25:08 +00:00
public config?: ResolvedConfigOptions;
2022-12-21 10:17:53 +00:00
private initPromise?: Promise<void>;
2022-11-03 19:43:41 +01:00
}
/**
* 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));
}
2026-06-05 11:49:43 +02:00
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;
}
2025-03-12 17:00:44 +00:00
async function downloadConfig(fetchTarget: string): Promise<ConfigOptions> {
const response = await fetch(fetchTarget);
2022-11-03 19:43:41 +01:00
if (isFailure(response)) {
2022-11-03 19:43:41 +01:00
// 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.
2022-12-20 17:26:45 +00:00
return DEFAULT_CONFIG;
2022-11-03 19:43:41 +01:00
}
return response.json();
2022-11-03 19:43:41 +01:00
}