557 lines
23 KiB
Swift
557 lines
23 KiB
Swift
import ClawdbotProtocol
|
|
import Foundation
|
|
|
|
extension ConnectionsStore {
|
|
func loadConfig() async {
|
|
do {
|
|
let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded(
|
|
method: .configGet,
|
|
params: nil,
|
|
timeoutMs: 10000)
|
|
self.configStatus = snap.valid == false
|
|
? "Config invalid; fix it in ~/.clawdbot/clawdbot.json."
|
|
: nil
|
|
self.configRoot = snap.config?.mapValues { $0.foundationValue } ?? [:]
|
|
self.configLoaded = true
|
|
|
|
self.applyUIConfig(snap)
|
|
self.applyTelegramConfig(snap)
|
|
self.applyDiscordConfig(snap)
|
|
self.applySignalConfig(snap)
|
|
self.applyIMessageConfig(snap)
|
|
} catch {
|
|
self.configStatus = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func applyUIConfig(_ snap: ConfigSnapshot) {
|
|
let ui = snap.config?[
|
|
"ui",
|
|
]?.dictionaryValue
|
|
let rawSeam = ui?[
|
|
"seamColor",
|
|
]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
AppStateStore.shared.seamColorHex = rawSeam.isEmpty ? nil : rawSeam
|
|
}
|
|
|
|
private func applyTelegramConfig(_ snap: ConfigSnapshot) {
|
|
let telegram = snap.config?["telegram"]?.dictionaryValue
|
|
self.telegramToken = telegram?["botToken"]?.stringValue ?? ""
|
|
self.telegramRequireMention = telegram?["requireMention"]?.boolValue ?? true
|
|
self.telegramAllowFrom = self.stringList(from: telegram?["allowFrom"]?.arrayValue)
|
|
self.telegramProxy = telegram?["proxy"]?.stringValue ?? ""
|
|
self.telegramWebhookUrl = telegram?["webhookUrl"]?.stringValue ?? ""
|
|
self.telegramWebhookSecret = telegram?["webhookSecret"]?.stringValue ?? ""
|
|
self.telegramWebhookPath = telegram?["webhookPath"]?.stringValue ?? ""
|
|
}
|
|
|
|
private func applyDiscordConfig(_ snap: ConfigSnapshot) {
|
|
let discord = snap.config?["discord"]?.dictionaryValue
|
|
self.discordEnabled = discord?["enabled"]?.boolValue ?? true
|
|
self.discordToken = discord?["token"]?.stringValue ?? ""
|
|
|
|
let discordDm = discord?["dm"]?.dictionaryValue
|
|
self.discordDmEnabled = discordDm?["enabled"]?.boolValue ?? true
|
|
self.discordAllowFrom = self.stringList(from: discordDm?["allowFrom"]?.arrayValue)
|
|
self.discordGroupEnabled = discordDm?["groupEnabled"]?.boolValue ?? false
|
|
self.discordGroupChannels = self.stringList(from: discordDm?["groupChannels"]?.arrayValue)
|
|
self.discordMediaMaxMb = self.numberString(from: discord?["mediaMaxMb"])
|
|
self.discordHistoryLimit = self.numberString(from: discord?["historyLimit"])
|
|
self.discordTextChunkLimit = self.numberString(from: discord?["textChunkLimit"])
|
|
self.discordReplyToMode = self.replyMode(from: discord?["replyToMode"]?.stringValue)
|
|
self.discordGuilds = self.decodeDiscordGuilds(discord?["guilds"]?.dictionaryValue)
|
|
|
|
let discordActions = discord?["actions"]?.dictionaryValue
|
|
self.discordActionReactions = discordActions?["reactions"]?.boolValue ?? true
|
|
self.discordActionStickers = discordActions?["stickers"]?.boolValue ?? true
|
|
self.discordActionPolls = discordActions?["polls"]?.boolValue ?? true
|
|
self.discordActionPermissions = discordActions?["permissions"]?.boolValue ?? true
|
|
self.discordActionMessages = discordActions?["messages"]?.boolValue ?? true
|
|
self.discordActionThreads = discordActions?["threads"]?.boolValue ?? true
|
|
self.discordActionPins = discordActions?["pins"]?.boolValue ?? true
|
|
self.discordActionSearch = discordActions?["search"]?.boolValue ?? true
|
|
self.discordActionMemberInfo = discordActions?["memberInfo"]?.boolValue ?? true
|
|
self.discordActionRoleInfo = discordActions?["roleInfo"]?.boolValue ?? true
|
|
self.discordActionChannelInfo = discordActions?["channelInfo"]?.boolValue ?? true
|
|
self.discordActionVoiceStatus = discordActions?["voiceStatus"]?.boolValue ?? true
|
|
self.discordActionEvents = discordActions?["events"]?.boolValue ?? true
|
|
self.discordActionRoles = discordActions?["roles"]?.boolValue ?? false
|
|
self.discordActionModeration = discordActions?["moderation"]?.boolValue ?? false
|
|
|
|
let slash = discord?["slashCommand"]?.dictionaryValue
|
|
self.discordSlashEnabled = slash?["enabled"]?.boolValue ?? false
|
|
self.discordSlashName = slash?["name"]?.stringValue ?? ""
|
|
self.discordSlashSessionPrefix = slash?["sessionPrefix"]?.stringValue ?? ""
|
|
self.discordSlashEphemeral = slash?["ephemeral"]?.boolValue ?? true
|
|
}
|
|
|
|
private func decodeDiscordGuilds(_ guilds: [String: AnyCodable]?) -> [DiscordGuildForm] {
|
|
guard let guilds else { return [] }
|
|
return guilds
|
|
.map { key, value in
|
|
let entry = value.dictionaryValue ?? [:]
|
|
let slug = entry["slug"]?.stringValue ?? ""
|
|
let requireMention = entry["requireMention"]?.boolValue ?? false
|
|
let reactionModeRaw = entry["reactionNotifications"]?.stringValue ?? ""
|
|
let reactionNotifications = ["off", "own", "all", "allowlist"].contains(reactionModeRaw)
|
|
? reactionModeRaw
|
|
: "own"
|
|
let users = self.stringList(from: entry["users"]?.arrayValue)
|
|
let channels: [DiscordGuildChannelForm] = if let channelMap = entry["channels"]?.dictionaryValue {
|
|
channelMap.map { channelKey, channelValue in
|
|
let channelEntry = channelValue.dictionaryValue ?? [:]
|
|
let allow = channelEntry["allow"]?.boolValue ?? true
|
|
let channelRequireMention = channelEntry["requireMention"]?.boolValue ?? false
|
|
return DiscordGuildChannelForm(
|
|
key: channelKey,
|
|
allow: allow,
|
|
requireMention: channelRequireMention)
|
|
}
|
|
} else {
|
|
[]
|
|
}
|
|
return DiscordGuildForm(
|
|
key: key,
|
|
slug: slug,
|
|
requireMention: requireMention,
|
|
reactionNotifications: reactionNotifications,
|
|
users: users,
|
|
channels: channels)
|
|
}
|
|
.sorted { $0.key < $1.key }
|
|
}
|
|
|
|
private func applySignalConfig(_ snap: ConfigSnapshot) {
|
|
let signal = snap.config?["signal"]?.dictionaryValue
|
|
self.signalEnabled = signal?["enabled"]?.boolValue ?? true
|
|
self.signalAccount = signal?["account"]?.stringValue ?? ""
|
|
self.signalHttpUrl = signal?["httpUrl"]?.stringValue ?? ""
|
|
self.signalHttpHost = signal?["httpHost"]?.stringValue ?? ""
|
|
self.signalHttpPort = self.numberString(from: signal?["httpPort"])
|
|
self.signalCliPath = signal?["cliPath"]?.stringValue ?? ""
|
|
self.signalAutoStart = signal?["autoStart"]?.boolValue ?? true
|
|
self.signalReceiveMode = signal?["receiveMode"]?.stringValue ?? ""
|
|
self.signalIgnoreAttachments = signal?["ignoreAttachments"]?.boolValue ?? false
|
|
self.signalIgnoreStories = signal?["ignoreStories"]?.boolValue ?? false
|
|
self.signalSendReadReceipts = signal?["sendReadReceipts"]?.boolValue ?? false
|
|
self.signalAllowFrom = self.stringList(from: signal?["allowFrom"]?.arrayValue)
|
|
self.signalMediaMaxMb = self.numberString(from: signal?["mediaMaxMb"])
|
|
}
|
|
|
|
private func applyIMessageConfig(_ snap: ConfigSnapshot) {
|
|
let imessage = snap.config?["imessage"]?.dictionaryValue
|
|
self.imessageEnabled = imessage?["enabled"]?.boolValue ?? true
|
|
self.imessageCliPath = imessage?["cliPath"]?.stringValue ?? ""
|
|
self.imessageDbPath = imessage?["dbPath"]?.stringValue ?? ""
|
|
self.imessageService = imessage?["service"]?.stringValue ?? "auto"
|
|
self.imessageRegion = imessage?["region"]?.stringValue ?? ""
|
|
self.imessageAllowFrom = self.stringList(from: imessage?["allowFrom"]?.arrayValue)
|
|
self.imessageIncludeAttachments = imessage?["includeAttachments"]?.boolValue ?? false
|
|
self.imessageMediaMaxMb = self.numberString(from: imessage?["mediaMaxMb"])
|
|
}
|
|
|
|
func saveTelegramConfig() async {
|
|
guard !self.isSavingConfig else { return }
|
|
self.isSavingConfig = true
|
|
defer { self.isSavingConfig = false }
|
|
if !self.configLoaded {
|
|
await self.loadConfig()
|
|
}
|
|
|
|
var telegram: [String: Any] = (self.configRoot["telegram"] as? [String: Any]) ?? [:]
|
|
let token = self.trimmed(self.telegramToken)
|
|
if token.isEmpty {
|
|
telegram.removeValue(forKey: "botToken")
|
|
} else {
|
|
telegram["botToken"] = token
|
|
}
|
|
|
|
telegram["requireMention"] = self.telegramRequireMention
|
|
|
|
let allow = self.splitCsv(self.telegramAllowFrom)
|
|
if allow.isEmpty {
|
|
telegram.removeValue(forKey: "allowFrom")
|
|
} else {
|
|
telegram["allowFrom"] = allow
|
|
}
|
|
|
|
self.setOptionalString(&telegram, key: "proxy", value: self.telegramProxy)
|
|
self.setOptionalString(&telegram, key: "webhookUrl", value: self.telegramWebhookUrl)
|
|
self.setOptionalString(&telegram, key: "webhookSecret", value: self.telegramWebhookSecret)
|
|
self.setOptionalString(&telegram, key: "webhookPath", value: self.telegramWebhookPath)
|
|
|
|
self.setSection("telegram", payload: telegram)
|
|
await self.persistConfig()
|
|
}
|
|
|
|
func saveDiscordConfig() async {
|
|
guard !self.isSavingConfig else { return }
|
|
self.isSavingConfig = true
|
|
defer { self.isSavingConfig = false }
|
|
if !self.configLoaded {
|
|
await self.loadConfig()
|
|
}
|
|
|
|
let discord = self.buildDiscordConfig()
|
|
self.setSection("discord", payload: discord)
|
|
await self.persistConfig()
|
|
}
|
|
|
|
func saveSignalConfig() async {
|
|
guard !self.isSavingConfig else { return }
|
|
self.isSavingConfig = true
|
|
defer { self.isSavingConfig = false }
|
|
if !self.configLoaded {
|
|
await self.loadConfig()
|
|
}
|
|
|
|
var signal: [String: Any] = (self.configRoot["signal"] as? [String: Any]) ?? [:]
|
|
if self.signalEnabled {
|
|
signal.removeValue(forKey: "enabled")
|
|
} else {
|
|
signal["enabled"] = false
|
|
}
|
|
|
|
self.setOptionalString(&signal, key: "account", value: self.signalAccount)
|
|
self.setOptionalString(&signal, key: "httpUrl", value: self.signalHttpUrl)
|
|
self.setOptionalString(&signal, key: "httpHost", value: self.signalHttpHost)
|
|
self.setOptionalNumber(&signal, key: "httpPort", value: self.signalHttpPort)
|
|
self.setOptionalString(&signal, key: "cliPath", value: self.signalCliPath)
|
|
|
|
if self.signalAutoStart {
|
|
signal.removeValue(forKey: "autoStart")
|
|
} else {
|
|
signal["autoStart"] = false
|
|
}
|
|
|
|
self.setOptionalString(&signal, key: "receiveMode", value: self.signalReceiveMode)
|
|
|
|
self.setOptionalBool(&signal, key: "ignoreAttachments", value: self.signalIgnoreAttachments)
|
|
self.setOptionalBool(&signal, key: "ignoreStories", value: self.signalIgnoreStories)
|
|
self.setOptionalBool(&signal, key: "sendReadReceipts", value: self.signalSendReadReceipts)
|
|
|
|
let allow = self.splitCsv(self.signalAllowFrom)
|
|
if allow.isEmpty {
|
|
signal.removeValue(forKey: "allowFrom")
|
|
} else {
|
|
signal["allowFrom"] = allow
|
|
}
|
|
|
|
self.setOptionalNumber(&signal, key: "mediaMaxMb", value: self.signalMediaMaxMb)
|
|
|
|
self.setSection("signal", payload: signal)
|
|
await self.persistConfig()
|
|
}
|
|
|
|
func saveIMessageConfig() async {
|
|
guard !self.isSavingConfig else { return }
|
|
self.isSavingConfig = true
|
|
defer { self.isSavingConfig = false }
|
|
if !self.configLoaded {
|
|
await self.loadConfig()
|
|
}
|
|
|
|
var imessage: [String: Any] = (self.configRoot["imessage"] as? [String: Any]) ?? [:]
|
|
if self.imessageEnabled {
|
|
imessage.removeValue(forKey: "enabled")
|
|
} else {
|
|
imessage["enabled"] = false
|
|
}
|
|
|
|
self.setOptionalString(&imessage, key: "cliPath", value: self.imessageCliPath)
|
|
self.setOptionalString(&imessage, key: "dbPath", value: self.imessageDbPath)
|
|
|
|
let service = self.trimmed(self.imessageService)
|
|
if service.isEmpty || service == "auto" {
|
|
imessage.removeValue(forKey: "service")
|
|
} else {
|
|
imessage["service"] = service
|
|
}
|
|
|
|
self.setOptionalString(&imessage, key: "region", value: self.imessageRegion)
|
|
|
|
let allow = self.splitCsv(self.imessageAllowFrom)
|
|
if allow.isEmpty {
|
|
imessage.removeValue(forKey: "allowFrom")
|
|
} else {
|
|
imessage["allowFrom"] = allow
|
|
}
|
|
|
|
self.setOptionalBool(&imessage, key: "includeAttachments", value: self.imessageIncludeAttachments)
|
|
self.setOptionalNumber(&imessage, key: "mediaMaxMb", value: self.imessageMediaMaxMb)
|
|
|
|
self.setSection("imessage", payload: imessage)
|
|
await self.persistConfig()
|
|
}
|
|
|
|
private func buildDiscordConfig() -> [String: Any] {
|
|
var discord: [String: Any] = (self.configRoot["discord"] as? [String: Any]) ?? [:]
|
|
if self.discordEnabled {
|
|
discord.removeValue(forKey: "enabled")
|
|
} else {
|
|
discord["enabled"] = false
|
|
}
|
|
self.setOptionalString(&discord, key: "token", value: self.discordToken)
|
|
|
|
if let dm = self.buildDiscordDmConfig(base: discord["dm"] as? [String: Any] ?? [:]) {
|
|
discord["dm"] = dm
|
|
} else {
|
|
discord.removeValue(forKey: "dm")
|
|
}
|
|
|
|
self.setOptionalNumber(&discord, key: "mediaMaxMb", value: self.discordMediaMaxMb)
|
|
self.setOptionalInt(&discord, key: "historyLimit", value: self.discordHistoryLimit, allowZero: true)
|
|
self.setOptionalInt(&discord, key: "textChunkLimit", value: self.discordTextChunkLimit, allowZero: false)
|
|
|
|
let replyToMode = self.trimmed(self.discordReplyToMode)
|
|
if replyToMode.isEmpty || replyToMode == "off" {
|
|
discord.removeValue(forKey: "replyToMode")
|
|
} else if ["first", "all"].contains(replyToMode) {
|
|
discord["replyToMode"] = replyToMode
|
|
} else {
|
|
discord.removeValue(forKey: "replyToMode")
|
|
}
|
|
|
|
if let guilds = self.buildDiscordGuildsConfig() {
|
|
discord["guilds"] = guilds
|
|
} else {
|
|
discord.removeValue(forKey: "guilds")
|
|
}
|
|
|
|
if let actions = self.buildDiscordActionsConfig(base: discord["actions"] as? [String: Any] ?? [:]) {
|
|
discord["actions"] = actions
|
|
} else {
|
|
discord.removeValue(forKey: "actions")
|
|
}
|
|
|
|
if let slash = self.buildDiscordSlashConfig(base: discord["slashCommand"] as? [String: Any] ?? [:]) {
|
|
discord["slashCommand"] = slash
|
|
} else {
|
|
discord.removeValue(forKey: "slashCommand")
|
|
}
|
|
|
|
return discord
|
|
}
|
|
|
|
private func buildDiscordDmConfig(base: [String: Any]) -> [String: Any]? {
|
|
var dm = base
|
|
if self.discordDmEnabled {
|
|
dm.removeValue(forKey: "enabled")
|
|
} else {
|
|
dm["enabled"] = false
|
|
}
|
|
let allow = self.splitCsv(self.discordAllowFrom)
|
|
if allow.isEmpty {
|
|
dm.removeValue(forKey: "allowFrom")
|
|
} else {
|
|
dm["allowFrom"] = allow
|
|
}
|
|
|
|
if self.discordGroupEnabled {
|
|
dm["groupEnabled"] = true
|
|
} else {
|
|
dm.removeValue(forKey: "groupEnabled")
|
|
}
|
|
|
|
let groupChannels = self.splitCsv(self.discordGroupChannels)
|
|
if groupChannels.isEmpty {
|
|
dm.removeValue(forKey: "groupChannels")
|
|
} else {
|
|
dm["groupChannels"] = groupChannels
|
|
}
|
|
|
|
return dm.isEmpty ? nil : dm
|
|
}
|
|
|
|
private func buildDiscordGuildsConfig() -> [String: Any]? {
|
|
let guilds: [String: Any] = self.discordGuilds.reduce(into: [:]) { result, entry in
|
|
let key = self.trimmed(entry.key)
|
|
guard !key.isEmpty else { return }
|
|
var payload: [String: Any] = [:]
|
|
let slug = self.trimmed(entry.slug)
|
|
if !slug.isEmpty { payload["slug"] = slug }
|
|
if entry.requireMention { payload["requireMention"] = true }
|
|
if ["off", "own", "all", "allowlist"].contains(entry.reactionNotifications) {
|
|
payload["reactionNotifications"] = entry.reactionNotifications
|
|
}
|
|
let users = self.splitCsv(entry.users)
|
|
if !users.isEmpty { payload["users"] = users }
|
|
let channels: [String: Any] = entry.channels.reduce(into: [:]) { channelsResult, channel in
|
|
let channelKey = self.trimmed(channel.key)
|
|
guard !channelKey.isEmpty else { return }
|
|
var channelPayload: [String: Any] = [:]
|
|
if !channel.allow { channelPayload["allow"] = false }
|
|
if channel.requireMention { channelPayload["requireMention"] = true }
|
|
channelsResult[channelKey] = channelPayload
|
|
}
|
|
if !channels.isEmpty { payload["channels"] = channels }
|
|
result[key] = payload
|
|
}
|
|
return guilds.isEmpty ? nil : guilds
|
|
}
|
|
|
|
private func buildDiscordActionsConfig(base: [String: Any]) -> [String: Any]? {
|
|
var actions = base
|
|
self.setAction(&actions, key: "reactions", value: self.discordActionReactions, defaultValue: true)
|
|
self.setAction(&actions, key: "stickers", value: self.discordActionStickers, defaultValue: true)
|
|
self.setAction(&actions, key: "polls", value: self.discordActionPolls, defaultValue: true)
|
|
self.setAction(&actions, key: "permissions", value: self.discordActionPermissions, defaultValue: true)
|
|
self.setAction(&actions, key: "messages", value: self.discordActionMessages, defaultValue: true)
|
|
self.setAction(&actions, key: "threads", value: self.discordActionThreads, defaultValue: true)
|
|
self.setAction(&actions, key: "pins", value: self.discordActionPins, defaultValue: true)
|
|
self.setAction(&actions, key: "search", value: self.discordActionSearch, defaultValue: true)
|
|
self.setAction(&actions, key: "memberInfo", value: self.discordActionMemberInfo, defaultValue: true)
|
|
self.setAction(&actions, key: "roleInfo", value: self.discordActionRoleInfo, defaultValue: true)
|
|
self.setAction(&actions, key: "channelInfo", value: self.discordActionChannelInfo, defaultValue: true)
|
|
self.setAction(&actions, key: "voiceStatus", value: self.discordActionVoiceStatus, defaultValue: true)
|
|
self.setAction(&actions, key: "events", value: self.discordActionEvents, defaultValue: true)
|
|
self.setAction(&actions, key: "roles", value: self.discordActionRoles, defaultValue: false)
|
|
self.setAction(&actions, key: "moderation", value: self.discordActionModeration, defaultValue: false)
|
|
return actions.isEmpty ? nil : actions
|
|
}
|
|
|
|
private func buildDiscordSlashConfig(base: [String: Any]) -> [String: Any]? {
|
|
var slash = base
|
|
if self.discordSlashEnabled {
|
|
slash["enabled"] = true
|
|
} else {
|
|
slash.removeValue(forKey: "enabled")
|
|
}
|
|
self.setOptionalString(&slash, key: "name", value: self.discordSlashName)
|
|
self.setOptionalString(&slash, key: "sessionPrefix", value: self.discordSlashSessionPrefix)
|
|
if self.discordSlashEphemeral {
|
|
slash.removeValue(forKey: "ephemeral")
|
|
} else {
|
|
slash["ephemeral"] = false
|
|
}
|
|
return slash.isEmpty ? nil : slash
|
|
}
|
|
|
|
private func persistConfig() async {
|
|
do {
|
|
let data = try JSONSerialization.data(
|
|
withJSONObject: self.configRoot,
|
|
options: [.prettyPrinted, .sortedKeys])
|
|
guard let raw = String(data: data, encoding: .utf8) else {
|
|
self.configStatus = "Failed to encode config."
|
|
return
|
|
}
|
|
let params: [String: AnyCodable] = ["raw": AnyCodable(raw)]
|
|
_ = try await GatewayConnection.shared.requestRaw(
|
|
method: .configSet,
|
|
params: params,
|
|
timeoutMs: 10000)
|
|
self.configStatus = "Saved to ~/.clawdbot/clawdbot.json."
|
|
await self.refresh(probe: true)
|
|
} catch {
|
|
self.configStatus = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func setSection(_ key: String, payload: [String: Any]) {
|
|
if payload.isEmpty {
|
|
self.configRoot.removeValue(forKey: key)
|
|
} else {
|
|
self.configRoot[key] = payload
|
|
}
|
|
}
|
|
|
|
private func stringList(from values: [AnyCodable]?) -> String {
|
|
guard let values else { return "" }
|
|
let strings = values.compactMap { entry -> String? in
|
|
if let str = entry.stringValue { return str }
|
|
if let intVal = entry.intValue { return String(intVal) }
|
|
if let doubleVal = entry.doubleValue { return String(Int(doubleVal)) }
|
|
return nil
|
|
}
|
|
return strings.joined(separator: ", ")
|
|
}
|
|
|
|
private func numberString(from value: AnyCodable?) -> String {
|
|
if let number = value?.doubleValue ?? value?.intValue.map(Double.init) {
|
|
return String(Int(number))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
private func replyMode(from value: String?) -> String {
|
|
if let value, ["off", "first", "all"].contains(value) {
|
|
return value
|
|
}
|
|
return "off"
|
|
}
|
|
|
|
private func splitCsv(_ value: String) -> [String] {
|
|
value
|
|
.split(separator: ",")
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
private func trimmed(_ value: String) -> String {
|
|
value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
private func setOptionalString(_ target: inout [String: Any], key: String, value: String) {
|
|
let trimmed = self.trimmed(value)
|
|
if trimmed.isEmpty {
|
|
target.removeValue(forKey: key)
|
|
} else {
|
|
target[key] = trimmed
|
|
}
|
|
}
|
|
|
|
private func setOptionalNumber(_ target: inout [String: Any], key: String, value: String) {
|
|
let trimmed = self.trimmed(value)
|
|
if trimmed.isEmpty {
|
|
target.removeValue(forKey: key)
|
|
} else if let number = Double(trimmed) {
|
|
target[key] = number
|
|
}
|
|
}
|
|
|
|
private func setOptionalInt(
|
|
_ target: inout [String: Any],
|
|
key: String,
|
|
value: String,
|
|
allowZero: Bool)
|
|
{
|
|
let trimmed = self.trimmed(value)
|
|
if trimmed.isEmpty {
|
|
target.removeValue(forKey: key)
|
|
return
|
|
}
|
|
guard let number = Int(trimmed) else {
|
|
target.removeValue(forKey: key)
|
|
return
|
|
}
|
|
let isValid = allowZero ? number >= 0 : number > 0
|
|
guard isValid else {
|
|
target.removeValue(forKey: key)
|
|
return
|
|
}
|
|
target[key] = number
|
|
}
|
|
|
|
private func setOptionalBool(_ target: inout [String: Any], key: String, value: Bool) {
|
|
if value {
|
|
target[key] = true
|
|
} else {
|
|
target.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
private func setAction(
|
|
_ actions: inout [String: Any],
|
|
key: String,
|
|
value: Bool,
|
|
defaultValue: Bool)
|
|
{
|
|
if value == defaultValue {
|
|
actions.removeValue(forKey: key)
|
|
} else {
|
|
actions[key] = value
|
|
}
|
|
}
|
|
}
|