diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 72e098e..eba38da 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -3285,7 +3285,18 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options = return form } -const HERMES_CHANNEL_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk'] +const HERMES_CHANNEL_PLATFORMS = [ + 'telegram', + 'discord', + 'slack', + 'feishu', + 'dingtalk', + 'teams', + 'google_chat', + 'irc', + 'line', + 'simplex', +] function normalizeHermesPlatform(platform) { const p = String(platform || '').trim().toLowerCase() @@ -3301,6 +3312,11 @@ function putHermesString(form, source, key) { if (typeof value === 'string') form[toCamelCaseKey(key)] = value } +function putHermesScalarString(form, source, key) { + const value = source?.[key] + if (typeof value === 'string' || typeof value === 'number') form[toCamelCaseKey(key)] = String(value) +} + function putHermesBool(form, source, key) { const value = source?.[key] if (typeof value === 'boolean') form[toCamelCaseKey(key)] = value @@ -3332,6 +3348,13 @@ function putHermesEnvBool(form, envValues, envKey, formKey) { if (value !== undefined) form[formKey] = value } +function putHermesHomeChannel(form, entry) { + const home = entry?.home_channel && typeof entry.home_channel === 'object' ? entry.home_channel : null + if (!home) return + if (typeof home.chat_id === 'string') form.homeChannel = home.chat_id + if (typeof home.name === 'string') form.homeChannelName = home.name +} + function readHermesEnvValues() { const envPath = path.join(hermesHome(), '.env') const values = {} @@ -3424,6 +3447,79 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) { putHermesString(form, extra, 'client_secret') form.clientId = hermesEnvValue(envValues, 'DINGTALK_CLIENT_ID') || form.clientId || '' form.clientSecret = hermesEnvValue(envValues, 'DINGTALK_CLIENT_SECRET') || form.clientSecret || '' + } else if (platform === 'teams') { + for (const key of ['client_id', 'client_secret', 'tenant_id', 'service_url']) putHermesString(form, extra, key) + putHermesScalarString(form, extra, 'port') + putHermesHomeChannel(form, entry) + form.clientId = hermesEnvValue(envValues, 'TEAMS_CLIENT_ID') || form.clientId || '' + form.clientSecret = hermesEnvValue(envValues, 'TEAMS_CLIENT_SECRET') || form.clientSecret || '' + form.tenantId = hermesEnvValue(envValues, 'TEAMS_TENANT_ID') || form.tenantId || '' + form.port = hermesEnvValue(envValues, 'TEAMS_PORT') || form.port || '' + form.serviceUrl = hermesEnvValue(envValues, 'TEAMS_SERVICE_URL') || form.serviceUrl || '' + putHermesEnvString(form, envValues, 'TEAMS_ALLOWED_USERS', 'allowFrom') + putHermesEnvBool(form, envValues, 'TEAMS_ALLOW_ALL_USERS', 'allowAllUsers') + putHermesEnvString(form, envValues, 'TEAMS_HOME_CHANNEL', 'homeChannel') + putHermesEnvString(form, envValues, 'TEAMS_HOME_CHANNEL_NAME', 'homeChannelName') + } else if (platform === 'google_chat') { + for (const key of ['project_id', 'subscription_name', 'service_account_json']) putHermesString(form, extra, key) + putHermesHomeChannel(form, entry) + form.projectId = hermesEnvValue(envValues, 'GOOGLE_CHAT_PROJECT_ID') || hermesEnvValue(envValues, 'GOOGLE_CLOUD_PROJECT') || form.projectId || '' + form.subscriptionName = hermesEnvValue(envValues, 'GOOGLE_CHAT_SUBSCRIPTION_NAME') || hermesEnvValue(envValues, 'GOOGLE_CHAT_SUBSCRIPTION') || form.subscriptionName || '' + form.serviceAccountJson = hermesEnvValue(envValues, 'GOOGLE_CHAT_SERVICE_ACCOUNT_JSON') || hermesEnvValue(envValues, 'GOOGLE_APPLICATION_CREDENTIALS') || form.serviceAccountJson || '' + putHermesEnvString(form, envValues, 'GOOGLE_CHAT_ALLOWED_USERS', 'allowFrom') + putHermesEnvBool(form, envValues, 'GOOGLE_CHAT_ALLOW_ALL_USERS', 'allowAllUsers') + putHermesEnvString(form, envValues, 'GOOGLE_CHAT_HOME_CHANNEL', 'homeChannel') + putHermesEnvString(form, envValues, 'GOOGLE_CHAT_HOME_CHANNEL_NAME', 'homeChannelName') + } else if (platform === 'irc') { + for (const key of ['server', 'channel', 'nickname', 'server_password', 'nickserv_password']) putHermesString(form, extra, key) + putHermesScalarString(form, extra, 'port') + putHermesBool(form, extra, 'use_tls') + putHermesCsv(form, extra, 'allowed_users') + if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers + delete form.allowedUsers + putHermesHomeChannel(form, entry) + form.server = hermesEnvValue(envValues, 'IRC_SERVER') || form.server || '' + form.channel = hermesEnvValue(envValues, 'IRC_CHANNEL') || form.channel || '' + form.nickname = hermesEnvValue(envValues, 'IRC_NICKNAME') || form.nickname || '' + form.port = hermesEnvValue(envValues, 'IRC_PORT') || form.port || '' + putHermesEnvBool(form, envValues, 'IRC_USE_TLS', 'useTls') + form.serverPassword = hermesEnvValue(envValues, 'IRC_SERVER_PASSWORD') || form.serverPassword || '' + form.nickservPassword = hermesEnvValue(envValues, 'IRC_NICKSERV_PASSWORD') || form.nickservPassword || '' + putHermesEnvString(form, envValues, 'IRC_ALLOWED_USERS', 'allowFrom') + putHermesEnvBool(form, envValues, 'IRC_ALLOW_ALL_USERS', 'allowAllUsers') + putHermesEnvString(form, envValues, 'IRC_HOME_CHANNEL', 'homeChannel') + putHermesEnvString(form, envValues, 'IRC_HOME_CHANNEL_NAME', 'homeChannelName') + } else if (platform === 'line') { + for (const key of ['channel_access_token', 'channel_secret', 'host', 'public_url', 'slow_response_threshold']) putHermesString(form, extra, key) + putHermesScalarString(form, extra, 'port') + putHermesCsv(form, extra, 'allowed_users') + if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers + delete form.allowedUsers + putHermesCsv(form, extra, 'allowed_groups') + putHermesCsv(form, extra, 'allowed_rooms') + putHermesHomeChannel(form, entry) + form.channelAccessToken = hermesEnvValue(envValues, 'LINE_CHANNEL_ACCESS_TOKEN') || form.channelAccessToken || '' + form.channelSecret = hermesEnvValue(envValues, 'LINE_CHANNEL_SECRET') || form.channelSecret || '' + form.port = hermesEnvValue(envValues, 'LINE_PORT') || form.port || '' + form.host = hermesEnvValue(envValues, 'LINE_HOST') || form.host || '' + form.publicUrl = hermesEnvValue(envValues, 'LINE_PUBLIC_URL') || form.publicUrl || '' + putHermesEnvString(form, envValues, 'LINE_ALLOWED_USERS', 'allowFrom') + putHermesEnvString(form, envValues, 'LINE_ALLOWED_GROUPS', 'allowedGroups') + putHermesEnvString(form, envValues, 'LINE_ALLOWED_ROOMS', 'allowedRooms') + putHermesEnvBool(form, envValues, 'LINE_ALLOW_ALL_USERS', 'allowAllUsers') + putHermesEnvString(form, envValues, 'LINE_HOME_CHANNEL', 'homeChannel') + form.slowResponseThreshold = hermesEnvValue(envValues, 'LINE_SLOW_RESPONSE_THRESHOLD') || form.slowResponseThreshold || '' + } else if (platform === 'simplex') { + putHermesString(form, extra, 'ws_url') + putHermesCsv(form, extra, 'allowed_users') + if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers + delete form.allowedUsers + putHermesHomeChannel(form, entry) + form.wsUrl = hermesEnvValue(envValues, 'SIMPLEX_WS_URL') || form.wsUrl || '' + putHermesEnvString(form, envValues, 'SIMPLEX_ALLOWED_USERS', 'allowFrom') + putHermesEnvBool(form, envValues, 'SIMPLEX_ALLOW_ALL_USERS', 'allowAllUsers') + putHermesEnvString(form, envValues, 'SIMPLEX_HOME_CHANNEL', 'homeChannel') + putHermesEnvString(form, envValues, 'SIMPLEX_HOME_CHANNEL_NAME', 'homeChannelName') } putHermesString(form, extra, 'dm_policy') putHermesString(form, extra, 'group_policy') @@ -3450,6 +3546,26 @@ function setHermesExtra(entry, key, value) { entry.extra[key] = value } +function setHermesExtraInteger(entry, key, value) { + const raw = String(value ?? '').trim() + if (!raw) return + const parsed = Number.parseInt(raw, 10) + if (Number.isFinite(parsed)) setHermesExtra(entry, key, parsed) +} + +function setHermesHomeChannel(entry, form = {}) { + if (!Object.hasOwn(form, 'homeChannel')) return + const chatId = String(form.homeChannel || '').trim() + if (!chatId) { + deleteHermesEntryKey(entry, 'home_channel') + return + } + entry.home_channel = { + chat_id: chatId, + name: String(form.homeChannelName || '').trim() || chatId, + } +} + function deleteHermesEntryKey(entry, key) { if (entry && typeof entry === 'object') delete entry[key] } @@ -3468,6 +3584,9 @@ function normalizeHermesChannelForm(platform, form = {}) { if (Object.hasOwn(normalized, 'requireMention')) { normalized.requireMention = normalized.requireMention === true || normalized.requireMention === 'true' || normalized.requireMention === 'on' } + if (Object.hasOwn(normalized, 'allowAllUsers')) { + normalized.allowAllUsers = normalized.allowAllUsers === true || normalized.allowAllUsers === 'true' || normalized.allowAllUsers === 'on' + } if (platform === 'feishu') { normalized.domain = String(normalized.domain || '').trim() || 'feishu' normalized.connectionMode = String(normalized.connectionMode || '').trim() || 'websocket' @@ -3489,6 +3608,14 @@ function normalizeHermesChannelForm(platform, form = {}) { normalized.historyBackfillLimit = String(normalized.historyBackfillLimit || '').trim() normalized.replyToMode = String(normalized.replyToMode || '').trim() } + if (platform === 'irc') { + if (Object.hasOwn(normalized, 'useTls')) normalized.useTls = normalized.useTls === true || normalized.useTls === 'true' || normalized.useTls === 'on' + } + if (platform === 'line') { + for (const key of ['allowedGroups', 'allowedRooms']) { + if (Object.hasOwn(normalized, key)) normalized[key] = csvToStringArray(normalized[key]) + } + } return normalized } @@ -3544,6 +3671,40 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) { deleteHermesExtraKey(entry, 'client_secret') deleteHermesExtraKey(entry, 'allow_from') deleteHermesExtraKey(entry, 'group_allow_from') + } else if (normalizedPlatform === 'teams') { + deleteHermesExtraKey(entry, 'client_id') + deleteHermesExtraKey(entry, 'client_secret') + deleteHermesExtraKey(entry, 'tenant_id') + setHermesExtraInteger(entry, 'port', normalized.port) + setHermesExtra(entry, 'service_url', String(normalized.serviceUrl || '').trim()) + setHermesHomeChannel(entry, normalized) + } else if (normalizedPlatform === 'google_chat') { + setHermesExtra(entry, 'project_id', String(normalized.projectId || '').trim()) + setHermesExtra(entry, 'subscription_name', String(normalized.subscriptionName || '').trim()) + deleteHermesExtraKey(entry, 'service_account_json') + setHermesHomeChannel(entry, normalized) + } else if (normalizedPlatform === 'irc') { + setHermesExtra(entry, 'server', String(normalized.server || '').trim()) + setHermesExtraInteger(entry, 'port', normalized.port) + setHermesExtra(entry, 'nickname', String(normalized.nickname || '').trim()) + setHermesExtra(entry, 'channel', String(normalized.channel || '').trim()) + if (Object.hasOwn(normalized, 'useTls')) setHermesExtra(entry, 'use_tls', !!normalized.useTls) + deleteHermesExtraKey(entry, 'server_password') + deleteHermesExtraKey(entry, 'nickserv_password') + setHermesHomeChannel(entry, normalized) + } else if (normalizedPlatform === 'line') { + deleteHermesExtraKey(entry, 'channel_access_token') + deleteHermesExtraKey(entry, 'channel_secret') + setHermesExtraInteger(entry, 'port', normalized.port) + setHermesExtra(entry, 'host', String(normalized.host || '').trim()) + setHermesExtra(entry, 'public_url', String(normalized.publicUrl || '').trim()) + if (Array.isArray(normalized.allowedGroups)) setHermesExtra(entry, 'allowed_groups', normalized.allowedGroups) + if (Array.isArray(normalized.allowedRooms)) setHermesExtra(entry, 'allowed_rooms', normalized.allowedRooms) + setHermesExtra(entry, 'slow_response_threshold', String(normalized.slowResponseThreshold || '').trim()) + setHermesHomeChannel(entry, normalized) + } else if (normalizedPlatform === 'simplex') { + setHermesExtra(entry, 'ws_url', String(normalized.wsUrl || '').trim()) + setHermesHomeChannel(entry, normalized) } if (Object.hasOwn(normalized, 'dmPolicy')) setHermesExtra(entry, 'dm_policy', normalized.dmPolicy) if (Object.hasOwn(normalized, 'groupPolicy')) { @@ -3552,7 +3713,8 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) { } if (Object.hasOwn(normalized, 'requireMention')) setHermesExtra(entry, 'require_mention', !!normalized.requireMention) if (Array.isArray(normalized.allowFrom)) { - setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_users' : 'allow_from', normalized.allowFrom) + const allowKey = ['dingtalk', 'irc', 'line', 'simplex'].includes(normalizedPlatform) ? 'allowed_users' : 'allow_from' + setHermesExtra(entry, allowKey, normalized.allowFrom) } if (Array.isArray(normalized.groupAllowFrom)) { setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_chats' : 'group_allow_from', normalized.groupAllowFrom) @@ -3666,6 +3828,54 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) { updates.DINGTALK_ALLOWED_USERS = csvEnvValue(form.allowFrom) updates.DINGTALK_ALLOWED_CHATS = csvEnvValue(form.groupAllowFrom) if (Object.hasOwn(form, 'requireMention')) updates.DINGTALK_REQUIRE_MENTION = boolEnvValue(form.requireMention) + } else if (platform === 'teams') { + updates.TEAMS_CLIENT_ID = String(form.clientId || '').trim() + updates.TEAMS_CLIENT_SECRET = String(form.clientSecret || '').trim() + updates.TEAMS_TENANT_ID = String(form.tenantId || '').trim() + updates.TEAMS_PORT = String(form.port || '').trim() + updates.TEAMS_SERVICE_URL = String(form.serviceUrl || '').trim() + updates.TEAMS_ALLOWED_USERS = csvEnvValue(form.allowFrom) + if (Object.hasOwn(form, 'allowAllUsers')) updates.TEAMS_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers) + updates.TEAMS_HOME_CHANNEL = String(form.homeChannel || '').trim() + updates.TEAMS_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim() + } else if (platform === 'google_chat') { + updates.GOOGLE_CHAT_PROJECT_ID = String(form.projectId || '').trim() + updates.GOOGLE_CHAT_SUBSCRIPTION_NAME = String(form.subscriptionName || '').trim() + updates.GOOGLE_CHAT_SERVICE_ACCOUNT_JSON = String(form.serviceAccountJson || '').trim() + updates.GOOGLE_CHAT_ALLOWED_USERS = csvEnvValue(form.allowFrom) + if (Object.hasOwn(form, 'allowAllUsers')) updates.GOOGLE_CHAT_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers) + updates.GOOGLE_CHAT_HOME_CHANNEL = String(form.homeChannel || '').trim() + updates.GOOGLE_CHAT_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim() + } else if (platform === 'irc') { + updates.IRC_SERVER = String(form.server || '').trim() + updates.IRC_PORT = String(form.port || '').trim() + updates.IRC_NICKNAME = String(form.nickname || '').trim() + updates.IRC_CHANNEL = String(form.channel || '').trim() + if (Object.hasOwn(form, 'useTls')) updates.IRC_USE_TLS = boolEnvValue(form.useTls) + updates.IRC_SERVER_PASSWORD = String(form.serverPassword || '').trim() + updates.IRC_NICKSERV_PASSWORD = String(form.nickservPassword || '').trim() + updates.IRC_ALLOWED_USERS = csvEnvValue(form.allowFrom) + if (Object.hasOwn(form, 'allowAllUsers')) updates.IRC_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers) + updates.IRC_HOME_CHANNEL = String(form.homeChannel || '').trim() + updates.IRC_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim() + } else if (platform === 'line') { + updates.LINE_CHANNEL_ACCESS_TOKEN = String(form.channelAccessToken || '').trim() + updates.LINE_CHANNEL_SECRET = String(form.channelSecret || '').trim() + updates.LINE_PORT = String(form.port || '').trim() + updates.LINE_HOST = String(form.host || '').trim() + updates.LINE_PUBLIC_URL = String(form.publicUrl || '').trim() + updates.LINE_ALLOWED_USERS = csvEnvValue(form.allowFrom) + updates.LINE_ALLOWED_GROUPS = csvEnvValue(form.allowedGroups) + updates.LINE_ALLOWED_ROOMS = csvEnvValue(form.allowedRooms) + if (Object.hasOwn(form, 'allowAllUsers')) updates.LINE_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers) + updates.LINE_HOME_CHANNEL = String(form.homeChannel || '').trim() + updates.LINE_SLOW_RESPONSE_THRESHOLD = String(form.slowResponseThreshold || '').trim() + } else if (platform === 'simplex') { + updates.SIMPLEX_WS_URL = String(form.wsUrl || '').trim() + updates.SIMPLEX_ALLOWED_USERS = csvEnvValue(form.allowFrom) + if (Object.hasOwn(form, 'allowAllUsers')) updates.SIMPLEX_ALLOW_ALL_USERS = boolEnvValue(form.allowAllUsers) + updates.SIMPLEX_HOME_CHANNEL = String(form.homeChannel || '').trim() + updates.SIMPLEX_HOME_CHANNEL_NAME = String(form.homeChannelName || '').trim() } return updates } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 776ac41..0d5c162 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2152,7 +2152,18 @@ fn merge_env_file(existing: &str, managed_keys: &[&str], new_pairs: &[(String, S // 并同步 Hermes 运行时仍会读取的 .env 变量。 // --------------------------------------------------------------------------- -const HERMES_CHANNEL_PLATFORMS: [&str; 5] = ["telegram", "discord", "slack", "feishu", "dingtalk"]; +const HERMES_CHANNEL_PLATFORMS: [&str; 10] = [ + "telegram", + "discord", + "slack", + "feishu", + "dingtalk", + "teams", + "google_chat", + "irc", + "line", + "simplex", +]; fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> { let platform = platform.trim().to_ascii_lowercase(); @@ -2183,6 +2194,25 @@ fn yaml_string_field(map: &serde_yaml::Mapping, key: &str) -> Option { .map(|v| v.to_string()) } +fn yaml_scalar_string_field(map: &serde_yaml::Mapping, key: &str) -> Option { + let value = yaml_get(map, key)?; + if let Some(value) = value.as_str() { + Some(value.to_string()) + } else if let Some(value) = value.as_i64() { + Some(value.to_string()) + } else if let Some(value) = value.as_u64() { + Some(value.to_string()) + } else { + value.as_f64().map(|value| { + if value.fract() == 0.0 { + format!("{value:.0}") + } else { + value.to_string() + } + }) + } +} + fn yaml_bool_field(map: &serde_yaml::Mapping, key: &str) -> Option { yaml_get(map, key).and_then(|v| v.as_bool()) } @@ -2220,6 +2250,17 @@ fn insert_json_string_if_present( } } +fn insert_json_scalar_string_if_present( + form: &mut serde_json::Map, + source: &serde_yaml::Mapping, + yaml_key: &str, + json_key: &str, +) { + if let Some(value) = yaml_scalar_string_field(source, yaml_key) { + form.insert(json_key.to_string(), Value::String(value)); + } +} + fn insert_json_bool_if_present( form: &mut serde_json::Map, source: &serde_yaml::Mapping, @@ -2294,6 +2335,21 @@ fn put_json_bool_from_env( } } +fn insert_hermes_home_channel_if_present( + form: &mut serde_json::Map, + entry: &serde_yaml::Mapping, +) { + let Some(home) = yaml_get_mapping(entry, "home_channel") else { + return; + }; + if let Some(value) = yaml_string_field(home, "chat_id") { + form.insert("homeChannel".to_string(), Value::String(value)); + } + if let Some(value) = yaml_string_field(home, "name") { + form.insert("homeChannelName".to_string(), Value::String(value)); + } +} + fn build_hermes_channel_config_values( config: &serde_yaml::Value, env_values: &std::collections::HashMap, @@ -2471,6 +2527,230 @@ fn build_hermes_channel_config_values( "clientSecret", ); } + "teams" => { + for (yaml_key_name, json_key_name) in [ + ("client_id", "clientId"), + ("client_secret", "clientSecret"), + ("tenant_id", "tenantId"), + ("service_url", "serviceUrl"), + ] { + insert_json_string_if_present(&mut form, &extra, yaml_key_name, json_key_name); + } + insert_json_scalar_string_if_present(&mut form, &extra, "port", "port"); + insert_hermes_home_channel_if_present(&mut form, &entry); + put_json_string_from_env(&mut form, env_values, "TEAMS_CLIENT_ID", "clientId"); + put_json_string_from_env( + &mut form, + env_values, + "TEAMS_CLIENT_SECRET", + "clientSecret", + ); + put_json_string_from_env(&mut form, env_values, "TEAMS_TENANT_ID", "tenantId"); + put_json_string_from_env(&mut form, env_values, "TEAMS_PORT", "port"); + put_json_string_from_env(&mut form, env_values, "TEAMS_SERVICE_URL", "serviceUrl"); + put_json_string_from_env(&mut form, env_values, "TEAMS_ALLOWED_USERS", "allowFrom"); + put_json_bool_from_env( + &mut form, + env_values, + "TEAMS_ALLOW_ALL_USERS", + "allowAllUsers", + ); + put_json_string_from_env( + &mut form, + env_values, + "TEAMS_HOME_CHANNEL", + "homeChannel", + ); + put_json_string_from_env( + &mut form, + env_values, + "TEAMS_HOME_CHANNEL_NAME", + "homeChannelName", + ); + } + "google_chat" => { + for (yaml_key_name, json_key_name) in [ + ("project_id", "projectId"), + ("subscription_name", "subscriptionName"), + ("service_account_json", "serviceAccountJson"), + ] { + insert_json_string_if_present(&mut form, &extra, yaml_key_name, json_key_name); + } + insert_hermes_home_channel_if_present(&mut form, &entry); + if let Some(value) = hermes_env_value(env_values, "GOOGLE_CHAT_PROJECT_ID") + .or_else(|| hermes_env_value(env_values, "GOOGLE_CLOUD_PROJECT")) + { + form.insert("projectId".to_string(), Value::String(value)); + } + if let Some(value) = hermes_env_value(env_values, "GOOGLE_CHAT_SUBSCRIPTION_NAME") + .or_else(|| hermes_env_value(env_values, "GOOGLE_CHAT_SUBSCRIPTION")) + { + form.insert("subscriptionName".to_string(), Value::String(value)); + } + if let Some(value) = + hermes_env_value(env_values, "GOOGLE_CHAT_SERVICE_ACCOUNT_JSON") + .or_else(|| hermes_env_value(env_values, "GOOGLE_APPLICATION_CREDENTIALS")) + { + form.insert("serviceAccountJson".to_string(), Value::String(value)); + } + put_json_string_from_env( + &mut form, + env_values, + "GOOGLE_CHAT_ALLOWED_USERS", + "allowFrom", + ); + put_json_bool_from_env( + &mut form, + env_values, + "GOOGLE_CHAT_ALLOW_ALL_USERS", + "allowAllUsers", + ); + put_json_string_from_env( + &mut form, + env_values, + "GOOGLE_CHAT_HOME_CHANNEL", + "homeChannel", + ); + put_json_string_from_env( + &mut form, + env_values, + "GOOGLE_CHAT_HOME_CHANNEL_NAME", + "homeChannelName", + ); + } + "irc" => { + for (yaml_key_name, json_key_name) in [ + ("server", "server"), + ("channel", "channel"), + ("nickname", "nickname"), + ("server_password", "serverPassword"), + ("nickserv_password", "nickservPassword"), + ] { + insert_json_string_if_present(&mut form, &extra, yaml_key_name, json_key_name); + } + insert_json_scalar_string_if_present(&mut form, &extra, "port", "port"); + insert_json_bool_if_present(&mut form, &extra, "use_tls", "useTls"); + insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); + insert_hermes_home_channel_if_present(&mut form, &entry); + put_json_string_from_env(&mut form, env_values, "IRC_SERVER", "server"); + put_json_string_from_env(&mut form, env_values, "IRC_CHANNEL", "channel"); + put_json_string_from_env(&mut form, env_values, "IRC_NICKNAME", "nickname"); + put_json_string_from_env(&mut form, env_values, "IRC_PORT", "port"); + put_json_bool_from_env(&mut form, env_values, "IRC_USE_TLS", "useTls"); + put_json_string_from_env( + &mut form, + env_values, + "IRC_SERVER_PASSWORD", + "serverPassword", + ); + put_json_string_from_env( + &mut form, + env_values, + "IRC_NICKSERV_PASSWORD", + "nickservPassword", + ); + put_json_string_from_env(&mut form, env_values, "IRC_ALLOWED_USERS", "allowFrom"); + put_json_bool_from_env( + &mut form, + env_values, + "IRC_ALLOW_ALL_USERS", + "allowAllUsers", + ); + put_json_string_from_env(&mut form, env_values, "IRC_HOME_CHANNEL", "homeChannel"); + put_json_string_from_env( + &mut form, + env_values, + "IRC_HOME_CHANNEL_NAME", + "homeChannelName", + ); + } + "line" => { + for (yaml_key_name, json_key_name) in [ + ("channel_access_token", "channelAccessToken"), + ("channel_secret", "channelSecret"), + ("host", "host"), + ("public_url", "publicUrl"), + ("slow_response_threshold", "slowResponseThreshold"), + ] { + insert_json_string_if_present(&mut form, &extra, yaml_key_name, json_key_name); + } + insert_json_scalar_string_if_present(&mut form, &extra, "port", "port"); + insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); + insert_json_csv_if_present(&mut form, &extra, "allowed_groups", "allowedGroups"); + insert_json_csv_if_present(&mut form, &extra, "allowed_rooms", "allowedRooms"); + insert_hermes_home_channel_if_present(&mut form, &entry); + put_json_string_from_env( + &mut form, + env_values, + "LINE_CHANNEL_ACCESS_TOKEN", + "channelAccessToken", + ); + put_json_string_from_env( + &mut form, + env_values, + "LINE_CHANNEL_SECRET", + "channelSecret", + ); + put_json_string_from_env(&mut form, env_values, "LINE_PORT", "port"); + put_json_string_from_env(&mut form, env_values, "LINE_HOST", "host"); + put_json_string_from_env(&mut form, env_values, "LINE_PUBLIC_URL", "publicUrl"); + put_json_string_from_env(&mut form, env_values, "LINE_ALLOWED_USERS", "allowFrom"); + put_json_string_from_env( + &mut form, + env_values, + "LINE_ALLOWED_GROUPS", + "allowedGroups", + ); + put_json_string_from_env( + &mut form, + env_values, + "LINE_ALLOWED_ROOMS", + "allowedRooms", + ); + put_json_bool_from_env( + &mut form, + env_values, + "LINE_ALLOW_ALL_USERS", + "allowAllUsers", + ); + put_json_string_from_env(&mut form, env_values, "LINE_HOME_CHANNEL", "homeChannel"); + put_json_string_from_env( + &mut form, + env_values, + "LINE_SLOW_RESPONSE_THRESHOLD", + "slowResponseThreshold", + ); + } + "simplex" => { + insert_json_string_if_present(&mut form, &extra, "ws_url", "wsUrl"); + insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); + insert_hermes_home_channel_if_present(&mut form, &entry); + put_json_string_from_env(&mut form, env_values, "SIMPLEX_WS_URL", "wsUrl"); + put_json_string_from_env( + &mut form, + env_values, + "SIMPLEX_ALLOWED_USERS", + "allowFrom", + ); + put_json_bool_from_env( + &mut form, + env_values, + "SIMPLEX_ALLOW_ALL_USERS", + "allowAllUsers", + ); + put_json_string_from_env( + &mut form, + env_values, + "SIMPLEX_HOME_CHANNEL", + "homeChannel", + ); + put_json_string_from_env( + &mut form, + env_values, + "SIMPLEX_HOME_CHANNEL_NAME", + "homeChannelName", + ); + } _ => {} } @@ -2480,6 +2760,8 @@ fn build_hermes_channel_config_values( if platform == "dingtalk" { insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); insert_json_csv_if_present(&mut form, &extra, "allowed_chats", "groupAllowFrom"); + } else if ["irc", "line", "simplex"].contains(&platform) { + insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); } else { insert_json_csv_if_present(&mut form, &extra, "allow_from", "allowFrom"); insert_json_csv_if_present(&mut form, &extra, "group_allow_from", "groupAllowFrom"); @@ -2531,6 +2813,14 @@ fn set_extra_string_if_present(entry: &mut serde_yaml::Mapping, key: &str, value } } +fn set_extra_integer_if_present(entry: &mut serde_yaml::Mapping, key: &str, value: Option) { + if let Some(value) = value { + if let Ok(extra) = yaml_child_object(entry, "extra") { + extra.insert(yaml_key(key), serde_yaml::Value::Number(value.into())); + } + } +} + fn delete_yaml_key(entry: &mut serde_yaml::Mapping, key: &str) { entry.remove(yaml_key(key)); } @@ -2570,6 +2860,25 @@ fn form_string(form: &Value, key: &str) -> Option { .map(|v| v.to_string()) } +fn form_i64(form: &Value, key: &str) -> Option { + let value = form.get(key)?; + if let Some(value) = value.as_i64() { + Some(value) + } else if let Some(value) = value.as_u64() { + i64::try_from(value).ok() + } else if let Some(value) = value.as_f64() { + if value.is_finite() { + Some(value as i64) + } else { + None + } + } else { + value + .as_str() + .and_then(|value| value.trim().parse::().ok()) + } +} + fn form_string_or_default(form: &Value, key: &str, default_value: &str) -> String { form_string(form, key) .map(|value| value.trim().to_string()) @@ -2608,6 +2917,27 @@ fn form_string_array(form: &Value, key: &str) -> Option> { Some(items) } +fn set_hermes_home_channel(entry: &mut serde_yaml::Mapping, form: &Value) { + if form.get("homeChannel").is_none() { + return; + } + let chat_id = form_string(form, "homeChannel") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + let Some(chat_id) = chat_id else { + delete_yaml_key(entry, "home_channel"); + return; + }; + let name = form_string(form, "homeChannelName") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| chat_id.clone()); + let mut home = serde_yaml::Mapping::new(); + home.insert(yaml_key("chat_id"), serde_yaml::Value::String(chat_id)); + home.insert(yaml_key("name"), serde_yaml::Value::String(name)); + entry.insert(yaml_key("home_channel"), serde_yaml::Value::Mapping(home)); +} + fn split_csv_items(value: &str) -> Vec { value .split([',', ';', '\n']) @@ -2739,6 +3069,59 @@ fn merge_hermes_channel_config( delete_extra_key(entry, "allow_from"); delete_extra_key(entry, "group_allow_from"); } + "teams" => { + delete_extra_key(entry, "client_id"); + delete_extra_key(entry, "client_secret"); + delete_extra_key(entry, "tenant_id"); + set_extra_integer_if_present(entry, "port", form_i64(form, "port")); + set_extra_string_if_present(entry, "service_url", form_string(form, "serviceUrl")); + set_hermes_home_channel(entry, form); + } + "google_chat" => { + set_extra_string_if_present(entry, "project_id", form_string(form, "projectId")); + set_extra_string_if_present( + entry, + "subscription_name", + form_string(form, "subscriptionName"), + ); + delete_extra_key(entry, "service_account_json"); + set_hermes_home_channel(entry, form); + } + "irc" => { + set_extra_string_if_present(entry, "server", form_string(form, "server")); + set_extra_integer_if_present(entry, "port", form_i64(form, "port")); + set_extra_string_if_present(entry, "nickname", form_string(form, "nickname")); + set_extra_string_if_present(entry, "channel", form_string(form, "channel")); + if let Some(value) = form_bool(form, "useTls") { + set_extra_bool(entry, "use_tls", value); + } + delete_extra_key(entry, "server_password"); + delete_extra_key(entry, "nickserv_password"); + set_hermes_home_channel(entry, form); + } + "line" => { + delete_extra_key(entry, "channel_access_token"); + delete_extra_key(entry, "channel_secret"); + set_extra_integer_if_present(entry, "port", form_i64(form, "port")); + set_extra_string_if_present(entry, "host", form_string(form, "host")); + set_extra_string_if_present(entry, "public_url", form_string(form, "publicUrl")); + if let Some(values) = form_string_array(form, "allowedGroups") { + set_extra_string_array(entry, "allowed_groups", values); + } + if let Some(values) = form_string_array(form, "allowedRooms") { + set_extra_string_array(entry, "allowed_rooms", values); + } + set_extra_string_if_present( + entry, + "slow_response_threshold", + form_string(form, "slowResponseThreshold"), + ); + set_hermes_home_channel(entry, form); + } + "simplex" => { + set_extra_string_if_present(entry, "ws_url", form_string(form, "wsUrl")); + set_hermes_home_channel(entry, form); + } _ => {} } @@ -2760,7 +3143,7 @@ fn merge_hermes_channel_config( set_extra_bool(entry, "require_mention", value); } if let Some(values) = form_string_array(form, "allowFrom") { - let key = if platform == "dingtalk" { + let key = if ["dingtalk", "irc", "line", "simplex"].contains(&platform) { "allowed_users" } else { "allow_from" @@ -2974,6 +3357,152 @@ fn build_hermes_channel_env_updates(platform: &str, form: &Value) -> Vec<(String push("DINGTALK_REQUIRE_MENTION", bool_env_value(value)); } } + "teams" => { + push( + "TEAMS_CLIENT_ID", + form_string(form, "clientId").unwrap_or_default(), + ); + push( + "TEAMS_CLIENT_SECRET", + form_string(form, "clientSecret").unwrap_or_default(), + ); + push( + "TEAMS_TENANT_ID", + form_string(form, "tenantId").unwrap_or_default(), + ); + push("TEAMS_PORT", form_string(form, "port").unwrap_or_default()); + push( + "TEAMS_SERVICE_URL", + form_string(form, "serviceUrl").unwrap_or_default(), + ); + push("TEAMS_ALLOWED_USERS", csv_env_value(form, "allowFrom")); + if let Some(value) = form_bool(form, "allowAllUsers") { + push("TEAMS_ALLOW_ALL_USERS", bool_env_value(value)); + } + push( + "TEAMS_HOME_CHANNEL", + form_string(form, "homeChannel").unwrap_or_default(), + ); + push( + "TEAMS_HOME_CHANNEL_NAME", + form_string(form, "homeChannelName").unwrap_or_default(), + ); + } + "google_chat" => { + push( + "GOOGLE_CHAT_PROJECT_ID", + form_string(form, "projectId").unwrap_or_default(), + ); + push( + "GOOGLE_CHAT_SUBSCRIPTION_NAME", + form_string(form, "subscriptionName").unwrap_or_default(), + ); + push( + "GOOGLE_CHAT_SERVICE_ACCOUNT_JSON", + form_string(form, "serviceAccountJson").unwrap_or_default(), + ); + push( + "GOOGLE_CHAT_ALLOWED_USERS", + csv_env_value(form, "allowFrom"), + ); + if let Some(value) = form_bool(form, "allowAllUsers") { + push("GOOGLE_CHAT_ALLOW_ALL_USERS", bool_env_value(value)); + } + push( + "GOOGLE_CHAT_HOME_CHANNEL", + form_string(form, "homeChannel").unwrap_or_default(), + ); + push( + "GOOGLE_CHAT_HOME_CHANNEL_NAME", + form_string(form, "homeChannelName").unwrap_or_default(), + ); + } + "irc" => { + push( + "IRC_SERVER", + form_string(form, "server").unwrap_or_default(), + ); + push("IRC_PORT", form_string(form, "port").unwrap_or_default()); + push( + "IRC_NICKNAME", + form_string(form, "nickname").unwrap_or_default(), + ); + push( + "IRC_CHANNEL", + form_string(form, "channel").unwrap_or_default(), + ); + if let Some(value) = form_bool(form, "useTls") { + push("IRC_USE_TLS", bool_env_value(value)); + } + push( + "IRC_SERVER_PASSWORD", + form_string(form, "serverPassword").unwrap_or_default(), + ); + push( + "IRC_NICKSERV_PASSWORD", + form_string(form, "nickservPassword").unwrap_or_default(), + ); + push("IRC_ALLOWED_USERS", csv_env_value(form, "allowFrom")); + if let Some(value) = form_bool(form, "allowAllUsers") { + push("IRC_ALLOW_ALL_USERS", bool_env_value(value)); + } + push( + "IRC_HOME_CHANNEL", + form_string(form, "homeChannel").unwrap_or_default(), + ); + push( + "IRC_HOME_CHANNEL_NAME", + form_string(form, "homeChannelName").unwrap_or_default(), + ); + } + "line" => { + push( + "LINE_CHANNEL_ACCESS_TOKEN", + form_string(form, "channelAccessToken").unwrap_or_default(), + ); + push( + "LINE_CHANNEL_SECRET", + form_string(form, "channelSecret").unwrap_or_default(), + ); + push("LINE_PORT", form_string(form, "port").unwrap_or_default()); + push("LINE_HOST", form_string(form, "host").unwrap_or_default()); + push( + "LINE_PUBLIC_URL", + form_string(form, "publicUrl").unwrap_or_default(), + ); + push("LINE_ALLOWED_USERS", csv_env_value(form, "allowFrom")); + push("LINE_ALLOWED_GROUPS", csv_env_value(form, "allowedGroups")); + push("LINE_ALLOWED_ROOMS", csv_env_value(form, "allowedRooms")); + if let Some(value) = form_bool(form, "allowAllUsers") { + push("LINE_ALLOW_ALL_USERS", bool_env_value(value)); + } + push( + "LINE_HOME_CHANNEL", + form_string(form, "homeChannel").unwrap_or_default(), + ); + push( + "LINE_SLOW_RESPONSE_THRESHOLD", + form_string(form, "slowResponseThreshold").unwrap_or_default(), + ); + } + "simplex" => { + push( + "SIMPLEX_WS_URL", + form_string(form, "wsUrl").unwrap_or_default(), + ); + push("SIMPLEX_ALLOWED_USERS", csv_env_value(form, "allowFrom")); + if let Some(value) = form_bool(form, "allowAllUsers") { + push("SIMPLEX_ALLOW_ALL_USERS", bool_env_value(value)); + } + push( + "SIMPLEX_HOME_CHANNEL", + form_string(form, "homeChannel").unwrap_or_default(), + ); + push( + "SIMPLEX_HOME_CHANNEL_NAME", + form_string(form, "homeChannelName").unwrap_or_default(), + ); + } _ => {} } @@ -3034,6 +3563,59 @@ fn write_hermes_channel_env(platform: &str, form: &Value) -> Result<(), String> "DINGTALK_ALLOWED_CHATS", "DINGTALK_REQUIRE_MENTION", ], + "teams" => vec![ + "TEAMS_CLIENT_ID", + "TEAMS_CLIENT_SECRET", + "TEAMS_TENANT_ID", + "TEAMS_PORT", + "TEAMS_SERVICE_URL", + "TEAMS_ALLOWED_USERS", + "TEAMS_ALLOW_ALL_USERS", + "TEAMS_HOME_CHANNEL", + "TEAMS_HOME_CHANNEL_NAME", + ], + "google_chat" => vec![ + "GOOGLE_CHAT_PROJECT_ID", + "GOOGLE_CHAT_SUBSCRIPTION_NAME", + "GOOGLE_CHAT_SERVICE_ACCOUNT_JSON", + "GOOGLE_CHAT_ALLOWED_USERS", + "GOOGLE_CHAT_ALLOW_ALL_USERS", + "GOOGLE_CHAT_HOME_CHANNEL", + "GOOGLE_CHAT_HOME_CHANNEL_NAME", + ], + "irc" => vec![ + "IRC_SERVER", + "IRC_PORT", + "IRC_NICKNAME", + "IRC_CHANNEL", + "IRC_USE_TLS", + "IRC_SERVER_PASSWORD", + "IRC_NICKSERV_PASSWORD", + "IRC_ALLOWED_USERS", + "IRC_ALLOW_ALL_USERS", + "IRC_HOME_CHANNEL", + "IRC_HOME_CHANNEL_NAME", + ], + "line" => vec![ + "LINE_CHANNEL_ACCESS_TOKEN", + "LINE_CHANNEL_SECRET", + "LINE_PORT", + "LINE_HOST", + "LINE_PUBLIC_URL", + "LINE_ALLOWED_USERS", + "LINE_ALLOWED_GROUPS", + "LINE_ALLOWED_ROOMS", + "LINE_ALLOW_ALL_USERS", + "LINE_HOME_CHANNEL", + "LINE_SLOW_RESPONSE_THRESHOLD", + ], + "simplex" => vec![ + "SIMPLEX_WS_URL", + "SIMPLEX_ALLOWED_USERS", + "SIMPLEX_ALLOW_ALL_USERS", + "SIMPLEX_HOME_CHANNEL", + "SIMPLEX_HOME_CHANNEL_NAME", + ], _ => Vec::new(), }; let pairs = build_hermes_channel_env_updates(platform, form); @@ -8412,4 +8994,340 @@ platforms: Some("keep-me") ); } + + #[test] + fn plugin_platform_values_prefer_env_and_preserve_yaml_runtime_fields() { + let config: serde_yaml::Value = serde_yaml::from_str( + r##" +platforms: + teams: + enabled: true + extra: + client_id: yaml-teams-client + client_secret: yaml-teams-secret + tenant_id: yaml-tenant + port: 3978 + service_url: https://smba.trafficmanager.net/teams/ + allow_from: ["aad-1"] + google_chat: + enabled: true + extra: + project_id: yaml-project + subscription_name: projects/yaml-project/subscriptions/hermes + service_account_json: yaml-sa.json + allow_from: ["user@example.com"] + irc: + enabled: true + extra: + server: irc.libera.chat + channel: "#hermes" + nickname: hermes-bot + use_tls: true + allowed_users: ["alice"] + line: + enabled: true + extra: + channel_access_token: yaml-line-token + channel_secret: yaml-line-secret + host: 0.0.0.0 + port: 8646 + public_url: https://line.example.com + allowed_users: ["U1"] + allowed_groups: ["C1"] + allowed_rooms: ["R1"] + slow_response_threshold: "45" + simplex: + enabled: true + extra: + ws_url: ws://127.0.0.1:5225 + allowed_users: ["contact-1"] +"##, + ) + .unwrap(); + let mut env = HashMap::new(); + env.insert( + "TEAMS_CLIENT_ID".to_string(), + "env-teams-client".to_string(), + ); + env.insert( + "TEAMS_CLIENT_SECRET".to_string(), + "env-teams-secret".to_string(), + ); + env.insert("TEAMS_TENANT_ID".to_string(), "env-tenant".to_string()); + env.insert("TEAMS_HOME_CHANNEL".to_string(), "teams-home".to_string()); + env.insert( + "GOOGLE_CHAT_PROJECT_ID".to_string(), + "env-project".to_string(), + ); + env.insert( + "GOOGLE_CHAT_SUBSCRIPTION_NAME".to_string(), + "projects/env-project/subscriptions/hermes".to_string(), + ); + env.insert( + "GOOGLE_CHAT_SERVICE_ACCOUNT_JSON".to_string(), + "env-sa.json".to_string(), + ); + env.insert( + "GOOGLE_CHAT_HOME_CHANNEL".to_string(), + "spaces/AAA".to_string(), + ); + env.insert("IRC_SERVER".to_string(), "irc.oftc.net".to_string()); + env.insert("IRC_CHANNEL".to_string(), "#ops".to_string()); + env.insert("IRC_NICKNAME".to_string(), "ops-bot".to_string()); + env.insert("IRC_HOME_CHANNEL".to_string(), "#reports".to_string()); + env.insert( + "LINE_CHANNEL_ACCESS_TOKEN".to_string(), + "env-line-token".to_string(), + ); + env.insert( + "LINE_CHANNEL_SECRET".to_string(), + "env-line-secret".to_string(), + ); + env.insert("LINE_HOME_CHANNEL".to_string(), "U-home".to_string()); + env.insert( + "SIMPLEX_WS_URL".to_string(), + "ws://127.0.0.1:5226".to_string(), + ); + env.insert( + "SIMPLEX_HOME_CHANNEL".to_string(), + "contact-home".to_string(), + ); + + let values = build_hermes_channel_config_values(&config, &env); + + assert_eq!(values["teams"]["clientId"], "env-teams-client"); + assert_eq!(values["teams"]["clientSecret"], "env-teams-secret"); + assert_eq!(values["teams"]["tenantId"], "env-tenant"); + assert_eq!(values["teams"]["homeChannel"], "teams-home"); + assert_eq!(values["teams"]["allowFrom"], "aad-1"); + assert_eq!(values["google_chat"]["projectId"], "env-project"); + assert_eq!( + values["google_chat"]["subscriptionName"], + "projects/env-project/subscriptions/hermes" + ); + assert_eq!(values["google_chat"]["serviceAccountJson"], "env-sa.json"); + assert_eq!(values["google_chat"]["homeChannel"], "spaces/AAA"); + assert_eq!(values["irc"]["server"], "irc.oftc.net"); + assert_eq!(values["irc"]["channel"], "#ops"); + assert_eq!(values["irc"]["nickname"], "ops-bot"); + assert_eq!(values["irc"]["homeChannel"], "#reports"); + assert_eq!(values["irc"]["useTls"], true); + assert_eq!(values["irc"]["allowFrom"], "alice"); + assert_eq!(values["line"]["channelAccessToken"], "env-line-token"); + assert_eq!(values["line"]["channelSecret"], "env-line-secret"); + assert_eq!(values["line"]["homeChannel"], "U-home"); + assert_eq!(values["line"]["allowedGroups"], "C1"); + assert_eq!(values["line"]["allowedRooms"], "R1"); + assert_eq!(values["simplex"]["wsUrl"], "ws://127.0.0.1:5226"); + assert_eq!(values["simplex"]["homeChannel"], "contact-home"); + assert_eq!(values["simplex"]["allowFrom"], "contact-1"); + } + + #[test] + fn plugin_platform_save_writes_runtime_fields_and_env() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + + merge_hermes_channel_config( + &mut config, + "teams", + &json!({ + "enabled": true, + "clientId": "teams-client", + "clientSecret": "teams-secret", + "tenantId": "tenant-1", + "port": "3978", + "serviceUrl": "https://smba.trafficmanager.net/teams/", + "allowFrom": "aad-1, aad-2", + "allowAllUsers": false, + "homeChannel": "19:abc@thread.tacv2", + "homeChannelName": "Ops", + }), + ) + .unwrap(); + + assert_eq!( + config["platforms"]["teams"]["extra"]["client_id"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["teams"]["extra"]["client_secret"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["teams"]["extra"]["tenant_id"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["teams"]["extra"]["port"].as_i64(), + Some(3978) + ); + assert_eq!( + config["platforms"]["teams"]["extra"]["service_url"].as_str(), + Some("https://smba.trafficmanager.net/teams/") + ); + assert_eq!( + config["platforms"]["teams"]["extra"]["allow_from"] + .as_sequence() + .unwrap() + .iter() + .filter_map(|item| item.as_str()) + .collect::>(), + vec!["aad-1", "aad-2"] + ); + + merge_hermes_channel_config( + &mut config, + "google_chat", + &json!({ + "enabled": true, + "projectId": "project-1", + "subscriptionName": "projects/project-1/subscriptions/hermes", + "serviceAccountJson": "C:\\keys\\sa.json", + "allowFrom": "user@example.com", + "allowAllUsers": true, + "homeChannel": "spaces/AAA", + "homeChannelName": "Ops Space", + }), + ) + .unwrap(); + + assert_eq!( + config["platforms"]["google_chat"]["extra"]["project_id"].as_str(), + Some("project-1") + ); + assert_eq!( + config["platforms"]["google_chat"]["extra"]["subscription_name"].as_str(), + Some("projects/project-1/subscriptions/hermes") + ); + assert_eq!( + config["platforms"]["google_chat"]["extra"]["service_account_json"], + serde_yaml::Value::Null + ); + + merge_hermes_channel_config( + &mut config, + "irc", + &json!({ + "enabled": true, + "server": "irc.libera.chat", + "port": "6697", + "nickname": "hermes-bot", + "channel": "#hermes", + "useTls": true, + "serverPassword": "server-secret", + "nickservPassword": "nick-secret", + "allowFrom": "alice, bob", + "allowAllUsers": false, + "homeChannel": "#reports", + "homeChannelName": "reports", + }), + ) + .unwrap(); + + assert_eq!( + config["platforms"]["irc"]["extra"]["server"].as_str(), + Some("irc.libera.chat") + ); + assert_eq!( + config["platforms"]["irc"]["extra"]["port"].as_i64(), + Some(6697) + ); + assert_eq!( + config["platforms"]["irc"]["extra"]["use_tls"].as_bool(), + Some(true) + ); + assert_eq!( + config["platforms"]["irc"]["extra"]["server_password"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["irc"]["extra"]["nickserv_password"], + serde_yaml::Value::Null + ); + + merge_hermes_channel_config( + &mut config, + "line", + &json!({ + "enabled": true, + "channelAccessToken": "line-token", + "channelSecret": "line-secret", + "port": "8646", + "host": "0.0.0.0", + "publicUrl": "https://line.example.com", + "allowFrom": "U1", + "allowedGroups": "C1", + "allowedRooms": "R1", + "allowAllUsers": false, + "homeChannel": "U-home", + "slowResponseThreshold": "45", + }), + ) + .unwrap(); + + assert_eq!( + config["platforms"]["line"]["extra"]["channel_access_token"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["line"]["extra"]["channel_secret"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["line"]["extra"]["port"].as_i64(), + Some(8646) + ); + assert_eq!( + config["platforms"]["line"]["extra"]["allowed_groups"] + .as_sequence() + .unwrap() + .iter() + .filter_map(|item| item.as_str()) + .collect::>(), + vec!["C1"] + ); + + merge_hermes_channel_config( + &mut config, + "simplex", + &json!({ + "enabled": true, + "wsUrl": "ws://127.0.0.1:5225", + "allowFrom": "contact-1", + "allowAllUsers": true, + "homeChannel": "group:ops", + "homeChannelName": "Ops", + }), + ) + .unwrap(); + + assert_eq!( + config["platforms"]["simplex"]["extra"]["ws_url"].as_str(), + Some("ws://127.0.0.1:5225") + ); + + let env = build_hermes_channel_env_updates( + "line", + &json!({ + "channelAccessToken": "line-token", + "channelSecret": "line-secret", + "port": "8646", + "host": "0.0.0.0", + "publicUrl": "https://line.example.com", + "allowFrom": "U1", + "allowedGroups": "C1", + "allowedRooms": "R1", + "allowAllUsers": false, + "homeChannel": "U-home", + "slowResponseThreshold": "45", + }), + ); + + assert!(env.contains(&( + "LINE_CHANNEL_ACCESS_TOKEN".to_string(), + "line-token".to_string() + ))); + assert!(env.contains(&("LINE_ALLOWED_GROUPS".to_string(), "C1".to_string()))); + assert!(env.contains(&("LINE_HOME_CHANNEL".to_string(), "U-home".to_string()))); + } } diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index e6fb0d7..beefccc 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -869,7 +869,7 @@ fn put_number_value_if_present(entry: &mut Map, key: &str, value: } return; } - put_number_from_form(entry, key, &value.and_then(|v| v.as_str()).unwrap_or("")); + put_number_from_form(entry, key, value.and_then(|v| v.as_str()).unwrap_or("")); } fn normalize_numeric_form_value(map: &mut Map, key: &str) { diff --git a/src/engines/hermes/pages/channels.js b/src/engines/hermes/pages/channels.js index 8ecae6b..29149c9 100644 --- a/src/engines/hermes/pages/channels.js +++ b/src/engines/hermes/pages/channels.js @@ -87,11 +87,112 @@ const CHANNELS = [ { key: 'clientSecret', labelKey: 'engine.hermesChannelDingTalkClientSecret', type: 'password', placeholder: 'client secret' }, ], }, + { + id: 'teams', + icon: 'users', + titleKey: 'engine.hermesChannelTeams', + descKey: 'engine.hermesChannelTeamsDesc', + secretFields: ['clientId', 'clientSecret', 'tenantId'], + fields: [ + { key: 'clientId', labelKey: 'engine.hermesChannelTeamsClientId', type: 'text', placeholder: '00000000-0000-0000-0000-000000000000' }, + { key: 'clientSecret', labelKey: 'engine.hermesChannelTeamsClientSecret', type: 'password', placeholder: 'client secret' }, + { key: 'tenantId', labelKey: 'engine.hermesChannelTeamsTenantId', type: 'text', placeholder: '00000000-0000-0000-0000-000000000000' }, + { key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '3978' }, + { key: 'serviceUrl', labelKey: 'engine.hermesChannelServiceUrl', type: 'url', placeholder: 'https://smba.trafficmanager.net/teams/' }, + ], + policyFields: [ + { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelTeamsAllowedUsersPh' }, + { key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' }, + { key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: '19:xxx@thread.tacv2' }, + { key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'ops' }, + ], + }, + { + id: 'google_chat', + icon: 'message-square', + titleKey: 'engine.hermesChannelGoogleChat', + descKey: 'engine.hermesChannelGoogleChatDesc', + secretFields: ['projectId', 'serviceAccountJson'], + fields: [ + { key: 'projectId', labelKey: 'engine.hermesChannelGoogleProjectId', type: 'text', placeholder: 'my-gcp-project' }, + { key: 'subscriptionName', labelKey: 'engine.hermesChannelGoogleSubscriptionName', type: 'text', placeholder: 'projects/my-gcp-project/subscriptions/hermes' }, + { key: 'serviceAccountJson', labelKey: 'engine.hermesChannelGoogleServiceAccount', type: 'password', placeholderKey: 'engine.hermesChannelGoogleServiceAccountPh' }, + ], + policyFields: [ + { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelGoogleAllowedUsersPh' }, + { key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' }, + { key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'spaces/AAAA...' }, + { key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'ops-space' }, + ], + }, + { + id: 'irc', + icon: 'hash', + titleKey: 'engine.hermesChannelIrc', + descKey: 'engine.hermesChannelIrcDesc', + secretFields: ['server', 'serverPassword', 'nickservPassword'], + fields: [ + { key: 'server', labelKey: 'engine.hermesChannelIrcServer', type: 'text', placeholder: 'irc.libera.chat' }, + { key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '6697' }, + { key: 'nickname', labelKey: 'engine.hermesChannelIrcNickname', type: 'text', placeholder: 'hermes-bot' }, + { key: 'channel', labelKey: 'engine.hermesChannelIrcChannel', type: 'text', placeholder: '#hermes' }, + { key: 'serverPassword', labelKey: 'engine.hermesChannelIrcServerPassword', type: 'password', placeholder: 'optional' }, + { key: 'nickservPassword', labelKey: 'engine.hermesChannelIrcNickservPassword', type: 'password', placeholder: 'optional' }, + ], + toggles: [ + { key: 'useTls', labelKey: 'engine.hermesChannelIrcUseTls' }, + ], + policyFields: [ + { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelIrcAllowedUsersPh' }, + { key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' }, + { key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: '#reports' }, + { key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'reports' }, + ], + }, + { + id: 'line', + icon: 'message-circle', + titleKey: 'engine.hermesChannelLine', + descKey: 'engine.hermesChannelLineDesc', + secretFields: ['channelAccessToken', 'channelSecret'], + fields: [ + { key: 'channelAccessToken', labelKey: 'engine.hermesChannelLineAccessToken', type: 'password', placeholder: 'LINE channel access token' }, + { key: 'channelSecret', labelKey: 'engine.hermesChannelLineSecret', type: 'password', placeholder: 'LINE channel secret' }, + { key: 'port', labelKey: 'engine.hermesChannelPort', type: 'number', placeholder: '8646' }, + { key: 'host', labelKey: 'engine.hermesChannelHost', type: 'text', placeholder: '0.0.0.0' }, + { key: 'publicUrl', labelKey: 'engine.hermesChannelPublicUrl', type: 'url', placeholder: 'https://line.example.com' }, + ], + policyFields: [ + { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedUsersPh' }, + { key: 'allowedGroups', labelKey: 'engine.hermesChannelLineAllowedGroups', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedGroupsPh' }, + { key: 'allowedRooms', labelKey: 'engine.hermesChannelLineAllowedRooms', type: 'textarea', placeholderKey: 'engine.hermesChannelLineAllowedRoomsPh' }, + { key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' }, + { key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'Uxxxxxxxx' }, + { key: 'slowResponseThreshold', labelKey: 'engine.hermesChannelLineSlowResponse', type: 'number', placeholder: '45' }, + ], + }, + { + id: 'simplex', + icon: 'radio', + titleKey: 'engine.hermesChannelSimpleX', + descKey: 'engine.hermesChannelSimpleXDesc', + secretFields: ['wsUrl'], + fields: [ + { key: 'wsUrl', labelKey: 'engine.hermesChannelSimpleXWsUrl', type: 'url', placeholder: 'ws://127.0.0.1:5225' }, + ], + policyFields: [ + { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowedUsers', type: 'textarea', placeholderKey: 'engine.hermesChannelSimpleXAllowedUsersPh' }, + { key: 'allowAllUsers', labelKey: 'engine.hermesChannelAllowAllUsers', type: 'checkbox' }, + { key: 'homeChannel', labelKey: 'engine.hermesChannelHomeChannel', type: 'text', placeholder: 'group:ops' }, + { key: 'homeChannelName', labelKey: 'engine.hermesChannelHomeChannelName', type: 'text', placeholder: 'Ops' }, + ], + }, ] -const COMMON_FIELDS = [ +const LEGACY_POLICY_FIELDS = [ { key: 'dmPolicy', labelKey: 'engine.hermesChannelDmPolicy', type: 'select', options: [['pair', 'engine.hermesChannelPolicyPair'], ['open', 'engine.hermesChannelPolicyOpen'], ['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['disabled', 'engine.hermesChannelPolicyDisabled']] }, { key: 'groupPolicy', labelKey: 'engine.hermesChannelGroupPolicy', type: 'select', options: [['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['open', 'engine.hermesChannelPolicyOpen'], ['disabled', 'engine.hermesChannelPolicyDisabled']] }, + { key: 'requireMention', labelKey: 'engine.hermesChannelRequireMention', type: 'checkbox' }, { key: 'allowFrom', labelKey: 'engine.hermesChannelAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelAllowFromPlaceholder' }, { key: 'groupAllowFrom', labelKey: 'engine.hermesChannelGroupAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelGroupAllowFromPlaceholder' }, ] @@ -109,13 +210,14 @@ function channelMeta(id) { } function defaultForm(platform) { - const form = { - enabled: false, - dmPolicy: 'pair', - groupPolicy: 'allowlist', - allowFrom: '', - groupAllowFrom: '', - requireMention: true, + const channel = channelMeta(platform) + const form = { enabled: false } + if (!channel.policyFields) { + form.dmPolicy = 'pair' + form.groupPolicy = 'allowlist' + form.allowFrom = '' + form.groupAllowFrom = '' + form.requireMention = true } if (platform === 'feishu') { form.domain = 'feishu' @@ -152,6 +254,15 @@ function isConfigured(channel, form) { function renderField(field, form, disabled) { const value = valueOf(form, field.key) const label = esc(t(field.labelKey)) + const placeholder = field.placeholderKey ? t(field.placeholderKey) : (field.placeholder || '') + if (field.type === 'checkbox') { + return ` + + ` + } if (field.type === 'select') { return `