import { SettingString, settingsType, SettingTopic, SettingTypeAtom, SettingValueMapping } from "../type/settingTypes"; import { CodingHelper } from "./codingHelper"; import SettingCommandHandler from "../command/management/setting/settingCommandHandler"; import SettingService from "../service/management/settingService"; import { APPLICATION_SECRET } from "../env.defaults"; import { BooleanConverter, EmailConverter, LongStringConverter, MsConverter, NumberConverter, StringConverter, TypeConverter, UrlConverter, } from "./convertHelper"; import cloneDeep from "lodash.clonedeep"; export default abstract class SettingHelper { private static settings: { [key in SettingString]?: string } = {}; private static listeners: Map void>> = new Map(); private static topicListeners: Map void>> = new Map(); private static readonly converters: Record> = { longstring: new LongStringConverter(), string: new StringConverter(), url: new UrlConverter(), number: new NumberConverter(), boolean: new BooleanConverter(), ms: new MsConverter(), email: new EmailConverter(), }; public static getAllSettings() { return cloneDeep(this.settings); } /** * Returns the value of a setting with the correct type based on the key * @param key The key of the setting * @returns The typed value of the setting */ public static getSetting(key: K): SettingValueMapping[K] { const settingType = settingsType[key]; const rawValue = this.settings[key] ?? String(settingType.default ?? ""); if (Array.isArray(settingType.type)) { return rawValue as unknown as SettingValueMapping[K]; } let processedValue = rawValue; if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue); } const baseType = typeof settingType.type === "string" ? (settingType.type.split("/")[0] as SettingTypeAtom) : (settingType.type as SettingTypeAtom); return this.converters[baseType].fromString(processedValue) as unknown as SettingValueMapping[K]; } /** * Sets a setting * @param key The key of the setting * @param value The value to set */ public static async setSetting(key: K, value: SettingValueMapping[K]): Promise { if (value === undefined || value === null) return; const stringValue = String(value); const settingType = settingsType[key]; this.validateSetting(key, stringValue); const oldValue = this.getSetting(key); let finalValue = stringValue; if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { finalValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue); } this.settings[key] = stringValue; const [topic, settingKey] = key.split(".") as [SettingTopic, string]; await SettingCommandHandler.create({ topic, key: settingKey, value: finalValue, }); const newValue = this.getSetting(key); this.notifyListeners(key, newValue, oldValue); } /** * Resets a setting to its default value * @param key The key of the setting */ public static async resetSetting(key: SettingString): Promise { if (this.getSetting(key) == String(settingsType[key].default ?? "")) return; const oldValue = this.getSetting(key); const settingType = settingsType[key]; this.settings[key] = String(settingType.default ?? ""); const [topic, settingKey] = key.split(".") as [SettingTopic, string]; await SettingCommandHandler.delete({ topic, key: settingKey, }); const newValue = this.getSetting(key); this.notifyListeners(key, newValue, oldValue); } public static async configure(): Promise { console.log("Configuring Settings"); const settings = await SettingService.getSettings(); for (const element of settings) { const ref = `${element.topic}.${element.key}` as SettingString; this.settings[ref] = element.value; try { this.validateSetting(ref); } catch (error) { console.warn(`Invalid setting ${ref}: ${error.message}`); } } } /** * Validates a setting * @param key The key of the setting * @param value Optional value to validate */ private static validateSetting(key: SettingString, value?: string): void { const settingType = settingsType[key]; const valueToCheck = value ?? this.settings[key] ?? String(settingType.default ?? ""); if (Array.isArray(settingType.type)) { return; } let processedValue = valueToCheck; // do not encypt data here - data is only crypted towards database // if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { // try { // processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue); // } catch (error) { // throw new Error(`Unable to decrypt value for ${key}: ${error.message}`); // } // } const baseType = typeof settingType.type === "string" ? (settingType.type.split("/")[0] as SettingTypeAtom) : (settingType.type as SettingTypeAtom); if (!this.converters[baseType].validate(processedValue)) { throw new Error(`Invalid value for ${key} of type ${baseType}`); } if (baseType === "number" && settingType.min !== undefined) { const numValue = Number(processedValue); if (numValue < settingType.min) { throw new Error(`${key} must be at least ${settingType.min}`); } } } /** * Registers a listener for changes to a specific setting * @param key The setting to monitor * @param callback Function to be called when changes occur */ public static onSettingChanged( key: K, callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void ): void { if (!this.listeners.has(key)) { this.listeners.set(key, []); } this.listeners.get(key)!.push(callback); } /** * Registers a listener for changes to a specific setting * @param key The setting to monitor * @param callback Function to be called when changes occur */ public static onSettingTopicChanged(key: K, callback: () => void): void { if (!this.topicListeners.has(key)) { this.topicListeners.set(key, []); } this.topicListeners.get(key)!.push(callback); } /** * Removes a registered listener * @param key The setting * @param callback The callback to remove */ public static removeSettingListener( key: K, callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void ): void { if (!this.listeners.has(key)) return; const callbacks = this.listeners.get(key)!; const index = callbacks.indexOf(callback); if (index !== -1) { callbacks.splice(index, 1); } if (callbacks.length === 0) { this.listeners.delete(key); } } /** * Notifies all registered listeners about changes * @param key The changed setting * @param newValue The new value * @param oldValue The old value */ private static notifyListeners(key: SettingString, newValue: any, oldValue: any): void { if (!this.listeners.has(key)) return; const callbacks = this.listeners.get(key)!; for (const callback of callbacks) { try { callback(newValue, oldValue); } catch (error) { console.error(`Error in setting listener for ${key}:`, error); } } const topicCallbacks = this.topicListeners.get(key.split(".")[0] as SettingTopic)!; for (const callback of topicCallbacks) { try { callback(); } catch (error) { console.error(`Error in setting listener for ${key.split(".")[0]}:`, error); } } } }