885 lines
31 KiB
Swift
885 lines
31 KiB
Swift
//
|
|
// SettingsService.swift
|
|
// oAI
|
|
//
|
|
// Settings persistence: SQLite for preferences, Keychain for API keys
|
|
//
|
|
|
|
import Foundation
|
|
import os
|
|
import Security
|
|
|
|
@Observable
|
|
class SettingsService {
|
|
static let shared = SettingsService()
|
|
|
|
// In-memory cache of DB settings for fast reads
|
|
private var cache: [String: String] = [:]
|
|
|
|
// Encrypted database keys (for API keys)
|
|
private enum EncryptedKeys {
|
|
static let openrouterAPIKey = "openrouterAPIKey"
|
|
static let anthropicAPIKey = "anthropicAPIKey"
|
|
static let openaiAPIKey = "openaiAPIKey"
|
|
static let googleAPIKey = "googleAPIKey"
|
|
static let googleSearchEngineID = "googleSearchEngineID"
|
|
}
|
|
|
|
// Old keychain keys (for migration only)
|
|
private enum KeychainKeys {
|
|
static let openrouterAPIKey = "com.oai.apikey.openrouter"
|
|
static let anthropicAPIKey = "com.oai.apikey.anthropic"
|
|
static let openaiAPIKey = "com.oai.apikey.openai"
|
|
static let googleAPIKey = "com.oai.apikey.google"
|
|
static let googleSearchEngineID = "com.oai.google.searchEngineID"
|
|
}
|
|
|
|
private init() {
|
|
// Load all settings from DB into cache
|
|
cache = (try? DatabaseService.shared.loadAllSettings()) ?? [:]
|
|
Log.settings.info("Settings initialized with \(self.cache.count) cached entries")
|
|
|
|
// Migrate from UserDefaults on first launch
|
|
migrateFromUserDefaultsIfNeeded()
|
|
|
|
// Migrate API keys from Keychain to encrypted database
|
|
migrateFromKeychainIfNeeded()
|
|
}
|
|
|
|
// MARK: - Provider Settings
|
|
|
|
var defaultProvider: Settings.Provider {
|
|
get {
|
|
if let raw = cache["defaultProvider"],
|
|
let provider = Settings.Provider(rawValue: raw) {
|
|
return provider
|
|
}
|
|
return .openrouter
|
|
}
|
|
set {
|
|
cache["defaultProvider"] = newValue.rawValue
|
|
DatabaseService.shared.setSetting(key: "defaultProvider", value: newValue.rawValue)
|
|
}
|
|
}
|
|
|
|
var defaultModel: String? {
|
|
get { cache["defaultModel"] }
|
|
set {
|
|
if let value = newValue {
|
|
cache["defaultModel"] = value
|
|
DatabaseService.shared.setSetting(key: "defaultModel", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "defaultModel")
|
|
DatabaseService.shared.deleteSetting(key: "defaultModel")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Model Settings
|
|
|
|
var streamEnabled: Bool {
|
|
get { cache["streamEnabled"].map { $0 == "true" } ?? true }
|
|
set {
|
|
cache["streamEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "streamEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var maxTokens: Int {
|
|
get { cache["maxTokens"].flatMap(Int.init) ?? 0 }
|
|
set {
|
|
cache["maxTokens"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "maxTokens", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var temperature: Double {
|
|
get { cache["temperature"].flatMap(Double.init) ?? 0.0 }
|
|
set {
|
|
cache["temperature"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "temperature", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var systemPrompt: String? {
|
|
get { cache["systemPrompt"] }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
cache["systemPrompt"] = value
|
|
DatabaseService.shared.setSetting(key: "systemPrompt", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "systemPrompt")
|
|
DatabaseService.shared.deleteSetting(key: "systemPrompt")
|
|
}
|
|
}
|
|
}
|
|
|
|
var customPromptMode: Settings.CustomPromptMode {
|
|
get {
|
|
if let rawValue = cache["customPromptMode"],
|
|
let mode = Settings.CustomPromptMode(rawValue: rawValue) {
|
|
return mode
|
|
}
|
|
return .append // Default to append mode
|
|
}
|
|
set {
|
|
cache["customPromptMode"] = newValue.rawValue
|
|
DatabaseService.shared.setSetting(key: "customPromptMode", value: newValue.rawValue)
|
|
}
|
|
}
|
|
|
|
// MARK: - Feature Settings
|
|
|
|
var onlineMode: Bool {
|
|
get { cache["onlineMode"] == "true" }
|
|
set {
|
|
cache["onlineMode"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "onlineMode", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var memoryEnabled: Bool {
|
|
get { cache["memoryEnabled"].map { $0 == "true" } ?? true }
|
|
set {
|
|
cache["memoryEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "memoryEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var contextSelectionEnabled: Bool {
|
|
get { cache["contextSelectionEnabled"] == "true" }
|
|
set {
|
|
cache["contextSelectionEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "contextSelectionEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var contextMaxTokens: Int {
|
|
get { cache["contextMaxTokens"].flatMap(Int.init) ?? 100_000 }
|
|
set {
|
|
cache["contextMaxTokens"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "contextMaxTokens", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var embeddingsEnabled: Bool {
|
|
get { cache["embeddingsEnabled"] == "true" }
|
|
set {
|
|
cache["embeddingsEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "embeddingsEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var embeddingProvider: String {
|
|
get { cache["embeddingProvider"] ?? "openai-small" }
|
|
set {
|
|
cache["embeddingProvider"] = newValue
|
|
DatabaseService.shared.setSetting(key: "embeddingProvider", value: newValue)
|
|
}
|
|
}
|
|
|
|
var progressiveSummarizationEnabled: Bool {
|
|
get { cache["progressiveSummarizationEnabled"] == "true" }
|
|
set {
|
|
cache["progressiveSummarizationEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "progressiveSummarizationEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var summarizationThreshold: Int {
|
|
get { cache["summarizationThreshold"].flatMap(Int.init) ?? 50 }
|
|
set {
|
|
cache["summarizationThreshold"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "summarizationThreshold", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var mcpEnabled: Bool {
|
|
get { cache["mcpEnabled"] == "true" }
|
|
set {
|
|
cache["mcpEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
// MARK: - Text Size Settings
|
|
|
|
/// GUI text size (headers, labels, buttons) — default 13
|
|
var guiTextSize: Double {
|
|
get { cache["guiTextSize"].flatMap(Double.init) ?? 13.0 }
|
|
set {
|
|
cache["guiTextSize"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "guiTextSize", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
/// Dialog/chat message text size — default 14
|
|
var dialogTextSize: Double {
|
|
get { cache["dialogTextSize"].flatMap(Double.init) ?? 14.0 }
|
|
set {
|
|
cache["dialogTextSize"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "dialogTextSize", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
/// Input box text size — default 14
|
|
var inputTextSize: Double {
|
|
get { cache["inputTextSize"].flatMap(Double.init) ?? 14.0 }
|
|
set {
|
|
cache["inputTextSize"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "inputTextSize", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
// MARK: - Toolbar Settings
|
|
|
|
/// Toolbar icon size — default 16 (minimum)
|
|
var toolbarIconSize: Double {
|
|
get { cache["toolbarIconSize"].flatMap(Double.init) ?? 16.0 }
|
|
set {
|
|
cache["toolbarIconSize"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "toolbarIconSize", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
/// Show labels on toolbar icons — default false
|
|
var showToolbarLabels: Bool {
|
|
get { cache["showToolbarLabels"] == "true" }
|
|
set {
|
|
cache["showToolbarLabels"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "showToolbarLabels", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
// MARK: - MCP Permissions
|
|
|
|
var mcpCanWriteFiles: Bool {
|
|
get { cache["mcpCanWriteFiles"] == "true" }
|
|
set {
|
|
cache["mcpCanWriteFiles"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpCanWriteFiles", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var mcpCanDeleteFiles: Bool {
|
|
get { cache["mcpCanDeleteFiles"] == "true" }
|
|
set {
|
|
cache["mcpCanDeleteFiles"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpCanDeleteFiles", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var mcpCanCreateDirectories: Bool {
|
|
get { cache["mcpCanCreateDirectories"] == "true" }
|
|
set {
|
|
cache["mcpCanCreateDirectories"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpCanCreateDirectories", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var mcpCanMoveFiles: Bool {
|
|
get { cache["mcpCanMoveFiles"] == "true" }
|
|
set {
|
|
cache["mcpCanMoveFiles"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpCanMoveFiles", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var mcpRespectGitignore: Bool {
|
|
get { cache["mcpRespectGitignore"].map { $0 == "true" } ?? true }
|
|
set {
|
|
cache["mcpRespectGitignore"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "mcpRespectGitignore", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
// MARK: - MCP Allowed Folders
|
|
|
|
var mcpAllowedFolders: [String] {
|
|
get {
|
|
guard let json = cache["mcpAllowedFolders"],
|
|
let data = json.data(using: .utf8),
|
|
let folders = try? JSONDecoder().decode([String].self, from: data) else {
|
|
return []
|
|
}
|
|
return folders
|
|
}
|
|
set {
|
|
if let data = try? JSONEncoder().encode(newValue),
|
|
let json = String(data: data, encoding: .utf8) {
|
|
cache["mcpAllowedFolders"] = json
|
|
DatabaseService.shared.setSetting(key: "mcpAllowedFolders", value: json)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Search Settings
|
|
|
|
var searchProvider: Settings.SearchProvider {
|
|
get {
|
|
if let raw = cache["searchProvider"],
|
|
let provider = Settings.SearchProvider(rawValue: raw) {
|
|
return provider
|
|
}
|
|
return .duckduckgo
|
|
}
|
|
set {
|
|
cache["searchProvider"] = newValue.rawValue
|
|
DatabaseService.shared.setSetting(key: "searchProvider", value: newValue.rawValue)
|
|
}
|
|
}
|
|
|
|
// MARK: - Ollama Settings
|
|
|
|
var ollamaBaseURL: String {
|
|
get { cache["ollamaBaseURL"] ?? "" }
|
|
set {
|
|
let trimmed = newValue.trimmingCharacters(in: .whitespaces)
|
|
if trimmed.isEmpty {
|
|
cache.removeValue(forKey: "ollamaBaseURL")
|
|
DatabaseService.shared.deleteSetting(key: "ollamaBaseURL")
|
|
} else {
|
|
cache["ollamaBaseURL"] = trimmed
|
|
DatabaseService.shared.setSetting(key: "ollamaBaseURL", value: trimmed)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolved Ollama URL — returns the user value or the default
|
|
var ollamaEffectiveURL: String {
|
|
let url = ollamaBaseURL
|
|
return url.isEmpty ? "http://localhost:11434" : url
|
|
}
|
|
|
|
/// Whether the user has explicitly configured an Ollama URL
|
|
var ollamaConfigured: Bool {
|
|
cache["ollamaBaseURL"] != nil && !(cache["ollamaBaseURL"]!.isEmpty)
|
|
}
|
|
|
|
// MARK: - API Keys (Encrypted Database)
|
|
|
|
var openrouterAPIKey: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openrouterAPIKey) }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openrouterAPIKey, value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openrouterAPIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
var anthropicAPIKey: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.anthropicAPIKey) }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.anthropicAPIKey, value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.anthropicAPIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
var openaiAPIKey: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.openaiAPIKey) }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.openaiAPIKey, value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.openaiAPIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
var googleAPIKey: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleAPIKey) }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleAPIKey, value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleAPIKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
var googleSearchEngineID: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: EncryptedKeys.googleSearchEngineID) }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: EncryptedKeys.googleSearchEngineID, value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: EncryptedKeys.googleSearchEngineID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Git Sync Settings
|
|
|
|
var syncEnabled: Bool {
|
|
get { cache["syncEnabled"] == "true" }
|
|
set {
|
|
cache["syncEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncRepoURL: String {
|
|
get { cache["syncRepoURL"] ?? "" }
|
|
set {
|
|
let trimmed = newValue.trimmingCharacters(in: .whitespaces)
|
|
if trimmed.isEmpty {
|
|
cache.removeValue(forKey: "syncRepoURL")
|
|
DatabaseService.shared.deleteSetting(key: "syncRepoURL")
|
|
} else {
|
|
cache["syncRepoURL"] = trimmed
|
|
DatabaseService.shared.setSetting(key: "syncRepoURL", value: trimmed)
|
|
}
|
|
}
|
|
}
|
|
|
|
var syncLocalPath: String {
|
|
get { cache["syncLocalPath"] ?? "~/Library/Application Support/oAI/sync" }
|
|
set {
|
|
cache["syncLocalPath"] = newValue
|
|
DatabaseService.shared.setSetting(key: "syncLocalPath", value: newValue)
|
|
}
|
|
}
|
|
|
|
var syncAuthMethod: String {
|
|
get { cache["syncAuthMethod"] ?? "token" } // Default to access token
|
|
set {
|
|
cache["syncAuthMethod"] = newValue
|
|
DatabaseService.shared.setSetting(key: "syncAuthMethod", value: newValue)
|
|
}
|
|
}
|
|
|
|
// Encrypted sync credentials
|
|
var syncUsername: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncUsername") }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: "syncUsername", value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: "syncUsername")
|
|
}
|
|
}
|
|
}
|
|
|
|
var syncPassword: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncPassword") }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: "syncPassword", value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: "syncPassword")
|
|
}
|
|
}
|
|
}
|
|
|
|
var syncAccessToken: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: "syncAccessToken") }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: "syncAccessToken", value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: "syncAccessToken")
|
|
}
|
|
}
|
|
}
|
|
|
|
var syncAutoExport: Bool {
|
|
get { cache["syncAutoExport"] == "true" }
|
|
set {
|
|
cache["syncAutoExport"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoExport", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoPull: Bool {
|
|
get { cache["syncAutoPull"] == "true" }
|
|
set {
|
|
cache["syncAutoPull"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoPull", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncConfigured: Bool {
|
|
guard !syncRepoURL.isEmpty else { return false }
|
|
|
|
switch syncAuthMethod {
|
|
case "ssh":
|
|
return true // SSH uses system keys
|
|
case "password":
|
|
return syncUsername != nil && syncPassword != nil
|
|
case "token":
|
|
return syncAccessToken != nil
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Auto-Sync Settings
|
|
|
|
var syncAutoSave: Bool {
|
|
get { cache["syncAutoSave"] == "true" }
|
|
set {
|
|
cache["syncAutoSave"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSave", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoSaveMinMessages: Int {
|
|
get { cache["syncAutoSaveMinMessages"].flatMap(Int.init) ?? 5 }
|
|
set {
|
|
cache["syncAutoSaveMinMessages"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSaveMinMessages", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoSaveOnModelSwitch: Bool {
|
|
get { cache["syncAutoSaveOnModelSwitch"] == "true" }
|
|
set {
|
|
cache["syncAutoSaveOnModelSwitch"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSaveOnModelSwitch", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoSaveOnAppQuit: Bool {
|
|
get { cache["syncAutoSaveOnAppQuit"] == "true" }
|
|
set {
|
|
cache["syncAutoSaveOnAppQuit"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSaveOnAppQuit", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoSaveOnIdle: Bool {
|
|
get { cache["syncAutoSaveOnIdle"] == "true" }
|
|
set {
|
|
cache["syncAutoSaveOnIdle"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSaveOnIdle", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncAutoSaveIdleMinutes: Int {
|
|
get { cache["syncAutoSaveIdleMinutes"].flatMap(Int.init) ?? 5 }
|
|
set {
|
|
cache["syncAutoSaveIdleMinutes"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "syncAutoSaveIdleMinutes", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var syncLastAutoSaveConversationId: String? {
|
|
get { cache["syncLastAutoSaveConversationId"] }
|
|
set {
|
|
if let value = newValue {
|
|
cache["syncLastAutoSaveConversationId"] = value
|
|
DatabaseService.shared.setSetting(key: "syncLastAutoSaveConversationId", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "syncLastAutoSaveConversationId")
|
|
DatabaseService.shared.deleteSetting(key: "syncLastAutoSaveConversationId")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Email Handler Settings
|
|
|
|
var emailHandlerEnabled: Bool {
|
|
get { cache["emailHandlerEnabled"] == "true" }
|
|
set {
|
|
cache["emailHandlerEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailHandlerEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailHandlerProvider: String {
|
|
get { cache["emailHandlerProvider"] ?? "openrouter" }
|
|
set {
|
|
cache["emailHandlerProvider"] = newValue
|
|
DatabaseService.shared.setSetting(key: "emailHandlerProvider", value: newValue)
|
|
}
|
|
}
|
|
|
|
var emailHandlerModel: String {
|
|
get { cache["emailHandlerModel"] ?? "" }
|
|
set {
|
|
cache["emailHandlerModel"] = newValue
|
|
DatabaseService.shared.setSetting(key: "emailHandlerModel", value: newValue)
|
|
}
|
|
}
|
|
|
|
var emailSubjectIdentifier: String {
|
|
get { cache["emailSubjectIdentifier"] ?? "[OAIBOT]" }
|
|
set {
|
|
cache["emailSubjectIdentifier"] = newValue
|
|
DatabaseService.shared.setSetting(key: "emailSubjectIdentifier", value: newValue)
|
|
}
|
|
}
|
|
|
|
var emailRateLimitEnabled: Bool {
|
|
get { cache["emailRateLimitEnabled"] == "true" }
|
|
set {
|
|
cache["emailRateLimitEnabled"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailRateLimitEnabled", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailRateLimitPerHour: Int {
|
|
get { cache["emailRateLimitPerHour"].flatMap(Int.init) ?? 10 }
|
|
set {
|
|
cache["emailRateLimitPerHour"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailRateLimitPerHour", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailMaxTokens: Int {
|
|
get { cache["emailMaxTokens"].flatMap(Int.init) ?? 2000 }
|
|
set {
|
|
cache["emailMaxTokens"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailMaxTokens", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailHandlerSystemPrompt: String? {
|
|
get { cache["emailHandlerSystemPrompt"] }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
cache["emailHandlerSystemPrompt"] = value
|
|
DatabaseService.shared.setSetting(key: "emailHandlerSystemPrompt", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "emailHandlerSystemPrompt")
|
|
DatabaseService.shared.deleteSetting(key: "emailHandlerSystemPrompt")
|
|
}
|
|
}
|
|
}
|
|
|
|
var emailOnlineMode: Bool {
|
|
get { cache["emailOnlineMode"] == "true" }
|
|
set {
|
|
cache["emailOnlineMode"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailOnlineMode", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailHandlerConfigured: Bool {
|
|
guard emailHandlerEnabled else { return false }
|
|
guard !emailHandlerModel.isEmpty else { return false }
|
|
// Check if email server is configured
|
|
guard emailServerConfigured else { return false }
|
|
return true
|
|
}
|
|
|
|
// MARK: - Email Server Settings
|
|
|
|
var emailImapHost: String? {
|
|
get { cache["emailImapHost"] }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
cache["emailImapHost"] = value
|
|
DatabaseService.shared.setSetting(key: "emailImapHost", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "emailImapHost")
|
|
DatabaseService.shared.deleteSetting(key: "emailImapHost")
|
|
}
|
|
}
|
|
}
|
|
|
|
var emailSmtpHost: String? {
|
|
get { cache["emailSmtpHost"] }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
cache["emailSmtpHost"] = value
|
|
DatabaseService.shared.setSetting(key: "emailSmtpHost", value: value)
|
|
} else {
|
|
cache.removeValue(forKey: "emailSmtpHost")
|
|
DatabaseService.shared.deleteSetting(key: "emailSmtpHost")
|
|
}
|
|
}
|
|
}
|
|
|
|
var emailUsername: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: "emailUsername") }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: "emailUsername", value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: "emailUsername")
|
|
}
|
|
}
|
|
}
|
|
|
|
var emailPassword: String? {
|
|
get { try? DatabaseService.shared.getEncryptedSetting(key: "emailPassword") }
|
|
set {
|
|
if let value = newValue, !value.isEmpty {
|
|
try? DatabaseService.shared.setEncryptedSetting(key: "emailPassword", value: value)
|
|
} else {
|
|
DatabaseService.shared.deleteEncryptedSetting(key: "emailPassword")
|
|
}
|
|
}
|
|
}
|
|
|
|
var emailImapPort: Int {
|
|
get { cache["emailImapPort"].flatMap(Int.init) ?? 993 }
|
|
set {
|
|
cache["emailImapPort"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailImapPort", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailSmtpPort: Int {
|
|
get { cache["emailSmtpPort"].flatMap(Int.init) ?? 587 }
|
|
set {
|
|
cache["emailSmtpPort"] = String(newValue)
|
|
DatabaseService.shared.setSetting(key: "emailSmtpPort", value: String(newValue))
|
|
}
|
|
}
|
|
|
|
var emailServerConfigured: Bool {
|
|
guard let imapHost = emailImapHost, !imapHost.isEmpty else { return false }
|
|
guard let smtpHost = emailSmtpHost, !smtpHost.isEmpty else { return false }
|
|
guard let username = emailUsername, !username.isEmpty else { return false }
|
|
guard let password = emailPassword, !password.isEmpty else { return false }
|
|
return true
|
|
}
|
|
|
|
// MARK: - UserDefaults Migration
|
|
|
|
private func migrateFromUserDefaultsIfNeeded() {
|
|
// Skip if already migrated
|
|
guard cache["_migrated"] == nil else { return }
|
|
|
|
let defaults = UserDefaults.standard
|
|
let migrations: [(udKey: String, dbKey: String)] = [
|
|
("defaultProvider", "defaultProvider"),
|
|
("defaultModel", "defaultModel"),
|
|
("streamEnabled", "streamEnabled"),
|
|
("maxTokens", "maxTokens"),
|
|
("temperature", "temperature"),
|
|
("onlineMode", "onlineMode"),
|
|
("memoryEnabled", "memoryEnabled"),
|
|
("mcpEnabled", "mcpEnabled"),
|
|
("searchProvider", "searchProvider"),
|
|
("ollamaBaseURL", "ollamaBaseURL"),
|
|
]
|
|
|
|
for (udKey, dbKey) in migrations {
|
|
guard cache[dbKey] == nil else { continue }
|
|
|
|
if let stringVal = defaults.string(forKey: udKey) {
|
|
cache[dbKey] = stringVal
|
|
DatabaseService.shared.setSetting(key: dbKey, value: stringVal)
|
|
} else if defaults.object(forKey: udKey) != nil {
|
|
// Handle bool/int/double stored as non-string
|
|
let value: String
|
|
if let boolVal = defaults.object(forKey: udKey) as? Bool {
|
|
value = String(boolVal)
|
|
} else if defaults.integer(forKey: udKey) != 0 {
|
|
value = String(defaults.integer(forKey: udKey))
|
|
} else if defaults.double(forKey: udKey) != 0.0 {
|
|
value = String(defaults.double(forKey: udKey))
|
|
} else {
|
|
continue
|
|
}
|
|
cache[dbKey] = value
|
|
DatabaseService.shared.setSetting(key: dbKey, value: value)
|
|
}
|
|
}
|
|
|
|
// Mark migration complete
|
|
cache["_migrated"] = "true"
|
|
DatabaseService.shared.setSetting(key: "_migrated", value: "true")
|
|
}
|
|
|
|
// MARK: - Keychain Migration
|
|
|
|
private func migrateFromKeychainIfNeeded() {
|
|
// Skip if already migrated
|
|
guard cache["_keychain_migrated"] == nil else { return }
|
|
|
|
Log.settings.info("Migrating API keys from Keychain to encrypted database...")
|
|
|
|
let keysToMigrate: [(keychainKey: String, encryptedKey: String)] = [
|
|
(KeychainKeys.openrouterAPIKey, EncryptedKeys.openrouterAPIKey),
|
|
(KeychainKeys.anthropicAPIKey, EncryptedKeys.anthropicAPIKey),
|
|
(KeychainKeys.openaiAPIKey, EncryptedKeys.openaiAPIKey),
|
|
(KeychainKeys.googleAPIKey, EncryptedKeys.googleAPIKey),
|
|
(KeychainKeys.googleSearchEngineID, EncryptedKeys.googleSearchEngineID),
|
|
]
|
|
|
|
var migratedCount = 0
|
|
for (keychainKey, encryptedKey) in keysToMigrate {
|
|
// Read from keychain
|
|
if let value = getKeychainValue(for: keychainKey), !value.isEmpty {
|
|
// Write to encrypted database
|
|
do {
|
|
try DatabaseService.shared.setEncryptedSetting(key: encryptedKey, value: value)
|
|
// Delete from keychain after successful migration
|
|
deleteKeychainValue(for: keychainKey)
|
|
migratedCount += 1
|
|
Log.settings.info("Migrated \(encryptedKey) from Keychain to encrypted database")
|
|
} catch {
|
|
Log.settings.error("Failed to migrate \(encryptedKey): \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
|
|
Log.settings.info("Keychain migration complete: \(migratedCount) keys migrated")
|
|
|
|
// Mark migration complete
|
|
cache["_keychain_migrated"] = "true"
|
|
DatabaseService.shared.setSetting(key: "_keychain_migrated", value: "true")
|
|
}
|
|
|
|
// MARK: - Keychain Helpers (for migration only)
|
|
|
|
private func getKeychainValue(for key: String) -> String? {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrAccount as String: key,
|
|
kSecReturnData as String: true,
|
|
kSecMatchLimit as String: kSecMatchLimitOne
|
|
]
|
|
|
|
var dataTypeRef: AnyObject?
|
|
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
|
|
|
guard status == errSecSuccess,
|
|
let data = dataTypeRef as? Data,
|
|
let value = String(data: data, encoding: .utf8) else {
|
|
return nil
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
private func setKeychainValue(_ value: String, for key: String) {
|
|
guard let data = value.data(using: .utf8) else { return }
|
|
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
|
|
let attributes: [String: Any] = [
|
|
kSecValueData as String: data
|
|
]
|
|
|
|
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
|
|
if updateStatus == errSecItemNotFound {
|
|
var newItem = query
|
|
newItem[kSecValueData as String] = data
|
|
SecItemAdd(newItem as CFDictionary, nil)
|
|
}
|
|
}
|
|
|
|
private func deleteKeychainValue(for key: String) {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrAccount as String: key
|
|
]
|
|
|
|
SecItemDelete(query as CFDictionary)
|
|
}
|
|
}
|