Allow overriding site config URL via query parameter

Remodeled config dialog a bit and added a way to fetch
site config from any url via ?load=URL parameter.
This commit is contained in:
Marcel Hellkamp 2023-07-27 17:12:56 +02:00
parent 9a00596ff1
commit cd183e14c8
2 changed files with 65 additions and 62 deletions

View File

@ -2,8 +2,8 @@
import { sanatizeConfig, isServer, isLanguage, toQuery } from '@/config';
import { computed, ref } from 'vue';
import { arrayUnique } from '@/utils';
import { useClipboard } from '@vueuse/core'
import {type Config} from '@/types';
import { type Config } from '@/types';
import { siteConfigParam } from '@/config'
const emit = defineEmits(['update:modelValue'])
const modalDom = ref(null)
@ -96,10 +96,15 @@ const sourceCount = computed(() => {
return c;
})
const clip = useClipboard()
const rateLimitRisk = computed(() => {
return sourceCount.value / config.value.interval > 1
})
const fullConfig = computed(() => {
return JSON.stringify(config.value)
const saveConfigLink = computed(() => {
const json = JSON.stringify(config.value, undefined, 2)
const bytes = new TextEncoder().encode(json)
const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join("");
return "data:application/json;charset=utf-8;base64," + btoa(binString);
})
const fullUrl = computed(() => {
@ -312,54 +317,45 @@ const onSubmit = () => {
<div class="tab-pane" id="ctab-advanced" aria-labelledby="btab-advanced" role="tabpanel">
<div class="mb-3">
<label for="interval" class="form-label">Interval</label>
<label for="edit-interval" class="form-label">Update interval</label>
<div class="ms-5">
<input type="text" class="form-control" name="interval" v-model.lazy="formInterval">
<div class="form-text">Update interval in seconds.</div>
<input type="text" class="form-control" name="edit-interval" v-model.lazy="formInterval">
<div class="form-text">Number of seconds to wait between updates.</div>
</div>
</div>
<div class="mb-3">
<label for="limit" class="form-label">Limit results per source</label>
<label for="edit-limit" class="form-label">Results per source</label>
<div class="ms-5">
<input type="text" class="form-control" name="limit" v-model.lazy="formLimit">
<div class="form-text">Fetch this many new posts per request.</div>
<input type="text" class="form-control" name="edit-limit" v-model.lazy="formLimit">
<div class="form-text">Limit number of results per hashtag, account or timeline.</div>
</div>
</div>
<div class="mb-3">
<h6>Save config</h6>
<h6>Download config</h6>
<div class="ms-5">
<div class="input-group">
<input type="text" class="form-control" readonly :value="fullUrl">
<button v-if="clip.isSupported" class="btn btn-outline-secondary" type="button"
@click.prevent="clip.copy(fullUrl)">Copy</button>
<div class="form-text">
You can <a :href="saveConfigLink" target="_blank" download="wall-config.json">download</a> the
current configuration as a JSON file and either upload it as <code>wall-config.json</code>
to your own self-hosted Fediwall, or host it somewhere else and load it via the
<code>?{{ siteConfigParam }}=URL</code> query parameter.
</div>
<div class="form-text">Bookmark or share this <a :href="fullUrl" target="_blank">Fediwall link</a>.</div>
<div class="mt-3 input-group">
<input type="text" class="form-control" readonly :value="fullConfig">
<button v-if="clip.isSupported" class="btn btn-outline-secondary" type="button"
@click.prevent="clip.copy(fullUrl)">Copy</button>
</div>
<div class="form-text">Only useful if you plan to self-host Fediwall on your own domain.</div>
</div>
</div>
</div>
</div>
</form>
<div v-if="sourceCount / config.interval > 1" class="alert alert-warning mt-5" role="alert">
Checking {{ sourceCount }} sources every {{ formInterval }} seconds requires a high number
of API requests per second. Please reduce the number of servers, hashtags or accounts, or increase the update
interval.
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary float-end" form="settings-form">Apply</button>
<div v-if="rateLimitRisk" class="alert alert-warning mb-3" role="alert">
Current configuration updates {{ sourceCount }} sources every {{ formInterval }} seconds and may
run into issues with rate-limited servers. Reduce the number of sources (servers, hashtags, accounts)
or increase the update interval.
</div>
<button type="submit" class="btn btn-primary" form="settings-form">Apply config</button>
</div>
</div>
@ -371,5 +367,6 @@ const onSubmit = () => {
.form-label,
h6 {
font-weight: bolder;
}</style>
}
</style>

View File

@ -4,8 +4,9 @@ import { fallbackConfig, siteConfigUrl } from "@/defaults";
import { type Config } from '@/types';
let siteConfig: Config | null = null;
export const siteConfigParam = "load"
let siteConfig: Config | undefined;
let siteConfigSource: string|undefined = undefined;
const themes = ["dark", "light", "auto"];
const boolYes = ["", "y", "yes", "true"];
@ -183,10 +184,13 @@ export function fromQuery(query: string): Config {
return sanatizeConfig(config);
}
export function toQuery(config: Config): string {
export function toQuery(config: Config, userConfig?: string): string {
const params = new URLSearchParams();
const defaults = siteConfig || fallbackConfig;
if (siteConfigSource && siteConfigSource !== siteConfigUrl)
params.set(siteConfigParam, siteConfigSource)
for (const { names, to } of parameterDefinitions) {
const value = to(config)
if (value !== to(defaults))
@ -231,6 +235,15 @@ export function sanatizeConfig(config: any): Config {
return choices.includes(value) ? value : fallback;
}
// Migrate old configuration within same minor release
if (isString(config.server)) {
console.warn("DEPRECATED: Config parameter 'server' is now an array and called 'servers'.");
(config.servers ??= []).push(config.server);
}
if (isString(config.info))
config.showinfo = config.info == "top"
const fallback = siteConfig || fallbackConfig;
const result: Partial<Config> = {}
@ -268,35 +281,28 @@ export function sanatizeConfig(config: any): Config {
return result as Config;
}
async function loadSideConfig() {
let config;
try {
config = await (await fetch(siteConfigUrl)).json() || {};
} catch (e) {
console.warn("Site config failed to load, falling back to hard-coded defaults!")
return;
}
// Migrate old configuration within same minor release
if (isString(config.server)) {
console.warn("DEPRECATED: Config parameter 'server' is now an array and called 'servers'.");
(config.servers ??= []).push(config.server);
}
if (isString(config.info))
config.showinfo = config.info == "top"
return sanatizeConfig(config)
}
export async function loadConfig() {
const params = new URLSearchParams(window.location.search);
const loadUrl = params.get(siteConfigParam)?.trim()
const loadJson = async (url: string) => {
try {
const rs = await fetch(url, {cache: "reload", })
if(!rs.ok) throw new Error(`HTTP error! Status: ${rs.status}`);
siteConfig = sanatizeConfig(await rs.json() || {});
siteConfigSource = url
} catch (e) {
console.warn(`Failed to load (or parse) [${url}], falling back to defaults.`)
return;
}
}
if (!siteConfig && loadUrl)
await loadJson(loadUrl)
if (!siteConfig && siteConfigUrl)
siteConfig = await loadSideConfig() || null
await loadJson(siteConfigUrl)
if (!siteConfig)
siteConfig = sanatizeConfig(deepClone(fallbackConfig))
if (window.location.search)
return fromQuery(window.location.search)
return deepClone({ ...siteConfig })
return fromQuery(window.location.search)
}