"use strict"; import optionService from "../../services/options.js"; import log from "../../services/log.js"; import searchService from "../../services/search/services/search.js"; import ValidationError from "../../errors/validation_error.js"; import type { Request } from "express"; import { changeLanguage, getLocales } from "../../services/i18n.js"; import type { OptionNames } from "@triliumnext/commons"; // options allowed to be updated directly in the Options dialog const ALLOWED_OPTIONS = new Set([ "eraseEntitiesAfterTimeInSeconds", "eraseEntitiesAfterTimeScale", "protectedSessionTimeout", "protectedSessionTimeoutTimeScale", "revisionSnapshotTimeInterval", "revisionSnapshotTimeIntervalTimeScale", "revisionSnapshotNumberLimit", "zoomFactor", "theme", "codeBlockTheme", "codeBlockWordWrap", "codeNoteTheme", "syncServerHost", "syncServerTimeout", "syncProxy", "hoistedNoteId", "mainFontSize", "mainFontFamily", "treeFontSize", "treeFontFamily", "detailFontSize", "detailFontFamily", "monospaceFontSize", "monospaceFontFamily", "openNoteContexts", "vimKeymapEnabled", "codeLineWrapEnabled", "codeNotesMimeTypes", "spellCheckEnabled", "spellCheckLanguageCode", "imageMaxWidthHeight", "imageJpegQuality", "leftPaneWidth", "rightPaneWidth", "leftPaneVisible", "rightPaneVisible", "nativeTitleBarVisible", "headingStyle", "autoCollapseNoteTree", "autoReadonlySizeText", "autoReadonlySizeCode", "overrideThemeFonts", "dailyBackupEnabled", "weeklyBackupEnabled", "monthlyBackupEnabled", "maxContentWidth", "compressImages", "downloadImagesAutomatically", "minTocHeadings", "highlightsList", "checkForUpdates", "disableTray", "eraseUnusedAttachmentsAfterSeconds", "eraseUnusedAttachmentsAfterTimeScale", "disableTray", "customSearchEngineName", "customSearchEngineUrl", "promotedAttributesOpenInRibbon", "editedNotesOpenInRibbon", "locale", "formattingLocale", "firstDayOfWeek", "firstWeekOfYear", "minDaysInFirstWeek", "languages", "textNoteEditorType", "textNoteEditorMultilineToolbar", "layoutOrientation", "backgroundEffects", "allowedHtmlTags", "redirectBareDomain", "showLoginInShareTheme", "splitEditorOrientation", // AI/LLM integration options "aiEnabled", "aiTemperature", "aiSystemPrompt", "aiProviderPrecedence", "openaiApiKey", "openaiBaseUrl", "openaiDefaultModel", "openaiEmbeddingModel", "anthropicApiKey", "anthropicBaseUrl", "anthropicDefaultModel", "voyageApiKey", "voyageEmbeddingModel", "ollamaBaseUrl", "ollamaDefaultModel", "ollamaEmbeddingModel", "embeddingAutoUpdateEnabled", "embeddingDimensionStrategy", "embeddingProviderPrecedence", "embeddingSimilarityThreshold", "embeddingBatchSize", "embeddingUpdateInterval", "enableAutomaticIndexing", "maxNotesPerLlmQuery", // Embedding options "embeddingDefaultDimension", "mfaEnabled", "mfaMethod" ]); function getOptions() { const optionMap = optionService.getOptionMap(); const resultMap: Record = {}; for (const optionName in optionMap) { if (isAllowed(optionName)) { resultMap[optionName] = optionMap[optionName as OptionNames]; } } resultMap["isPasswordSet"] = optionMap["passwordVerificationHash"] ? "true" : "false"; return resultMap; } function updateOption(req: Request) { const { name, value } = req.params; if (!update(name, value)) { throw new ValidationError("not allowed option to change"); } } function updateOptions(req: Request) { for (const optionName in req.body) { if (!update(optionName, req.body[optionName])) { // this should be improved // it should return 400 instead of current 500, but at least it now rollbacks transaction throw new Error(`Option '${optionName}' is not allowed to be changed`); } } } function update(name: string, value: string) { if (!isAllowed(name)) { return false; } if (name !== "openNoteContexts") { log.info(`Updating option '${name}' to '${value}'`); } optionService.setOption(name as OptionNames, value); if (name === "locale") { // This runs asynchronously, so it's not perfect, but it does the trick for now. changeLanguage(value); } return true; } function getUserThemes() { const notes = searchService.searchNotes("#appTheme", { ignoreHoistedNote: true }); const ret = []; for (const note of notes) { let value = note.getOwnedLabelValue("appTheme"); if (!value) { value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, "-"); } ret.push({ val: value, title: note.title, noteId: note.noteId }); } return ret; } function getSupportedLocales() { return getLocales(); } function isAllowed(name: string) { return (ALLOWED_OPTIONS as Set).has(name) || name.startsWith("keyboardShortcuts") || name.endsWith("Collapsed") || name.startsWith("hideArchivedNotes"); } export default { getOptions, updateOption, updateOptions, getUserThemes, getSupportedLocales };