use crate::app_error::AppCommandError; use crate::chat_channel::backends::weixin::{WeixinQrcodeInfo, WeixinQrcodeStatusPublic}; use crate::chat_channel::manager::ChatChannelManager; use crate::chat_channel::types::ChannelType; use crate::db::service::{chat_channel_message_log_service, chat_channel_service}; use crate::db::AppDatabase; use crate::models::chat_channel::{ChannelStatusInfo, ChatChannelInfo, ChatChannelMessageLogInfo}; // --------------------------------------------------------------------------- // Shared core functions (used by both Tauri commands and web handlers) // --------------------------------------------------------------------------- pub async fn list_chat_channels_core( db: &AppDatabase, ) -> Result, AppCommandError> { let rows = chat_channel_service::list_all(&db.conn) .await .map_err(AppCommandError::from)?; Ok(rows.into_iter().map(ChatChannelInfo::from).collect()) } pub async fn create_chat_channel_core( db: &AppDatabase, name: String, channel_type: String, config_json: String, enabled: bool, daily_report_enabled: bool, daily_report_time: Option, ) -> Result { // Validate channel_type let _: ChannelType = serde_json::from_value(serde_json::Value::String(channel_type.clone())) .map_err(|_| { AppCommandError::invalid_input(format!("Invalid channel type: {channel_type}")) })?; let model = chat_channel_service::create( &db.conn, name, channel_type, config_json, enabled, daily_report_enabled, daily_report_time, ) .await .map_err(AppCommandError::from)?; Ok(ChatChannelInfo::from(model)) } #[allow(clippy::too_many_arguments)] pub async fn update_chat_channel_core( db: &AppDatabase, id: i32, name: Option, enabled: Option, config_json: Option, event_filter_json: Option>, daily_report_enabled: Option, daily_report_time: Option>, ) -> Result { let model = chat_channel_service::update( &db.conn, id, name, enabled, config_json, event_filter_json, daily_report_enabled, daily_report_time, ) .await .map_err(AppCommandError::from)?; Ok(ChatChannelInfo::from(model)) } pub async fn delete_chat_channel_core( db: &AppDatabase, manager: &ChatChannelManager, id: i32, ) -> Result<(), AppCommandError> { // Disconnect running backend before deleting from DB (prevents orphaned task) let _ = manager.remove_channel(id).await; chat_channel_service::delete(&db.conn, id) .await .map_err(AppCommandError::from)?; let _ = crate::keyring_store::delete_channel_token(id); Ok(()) } pub async fn connect_chat_channel_core( db: &AppDatabase, manager: &ChatChannelManager, id: i32, ) -> Result<(), AppCommandError> { let model = chat_channel_service::get_by_id(&db.conn, id) .await .map_err(AppCommandError::from)? .ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?; let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String(model.channel_type.clone())) .map_err(|_| { AppCommandError::configuration_invalid(format!( "Invalid channel type: {}", model.channel_type )) })?; let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| { AppCommandError::configuration_invalid("Invalid config JSON") .with_detail(e.to_string()) })?; let token = crate::keyring_store::get_channel_token(id).ok_or_else(|| { eprintln!("[connect_chat_channel] channel {id}: Token not set in keyring"); AppCommandError::configuration_missing("Token not set") })?; eprintln!( "[connect_chat_channel] channel {id}: creating {channel_type} backend, config={}", model.config_json ); let backend = crate::chat_channel::backends::create_backend(id, channel_type, &config, token) .map_err(AppCommandError::from)?; manager .add_channel(id, model.name, channel_type, backend) .await .map_err(|e| { eprintln!("[connect_chat_channel] channel {id}: add_channel failed: {e}"); AppCommandError::from(e) })?; eprintln!("[connect_chat_channel] channel {id}: connected successfully"); Ok(()) } pub async fn test_chat_channel_core( db: &AppDatabase, id: i32, ) -> Result<(), AppCommandError> { let model = chat_channel_service::get_by_id(&db.conn, id) .await .map_err(AppCommandError::from)? .ok_or_else(|| AppCommandError::not_found(format!("Chat channel {id} not found")))?; let channel_type: ChannelType = serde_json::from_value(serde_json::Value::String(model.channel_type.clone())) .map_err(|_| { AppCommandError::configuration_invalid(format!( "Invalid channel type: {}", model.channel_type )) })?; let config: serde_json::Value = serde_json::from_str(&model.config_json).map_err(|e| { AppCommandError::configuration_invalid("Invalid config JSON") .with_detail(e.to_string()) })?; let token = crate::keyring_store::get_channel_token(id) .ok_or_else(|| AppCommandError::configuration_missing("Token not set"))?; let backend = crate::chat_channel::backends::create_backend(id, channel_type, &config, token) .map_err(AppCommandError::from)?; backend .test_connection() .await .map_err(AppCommandError::from)?; Ok(()) } pub fn save_chat_channel_token_core(channel_id: i32, token: &str) -> Result<(), AppCommandError> { crate::keyring_store::set_channel_token(channel_id, token) .map_err(|e| AppCommandError::io_error("Failed to save token").with_detail(e)) } pub fn get_chat_channel_has_token_core(channel_id: i32) -> Result { Ok(crate::keyring_store::get_channel_token(channel_id).is_some()) } pub fn delete_chat_channel_token_core(channel_id: i32) -> Result<(), AppCommandError> { crate::keyring_store::delete_channel_token(channel_id) .map_err(|e| AppCommandError::io_error("Failed to delete token").with_detail(e)) } pub async fn disconnect_chat_channel_core( manager: &ChatChannelManager, id: i32, ) -> Result<(), AppCommandError> { manager .remove_channel(id) .await .map_err(AppCommandError::from)?; Ok(()) } pub async fn get_chat_channel_status_core( manager: &ChatChannelManager, ) -> Result, AppCommandError> { Ok(manager.get_status().await) } pub async fn list_chat_channel_messages_core( db: &AppDatabase, channel_id: i32, limit: Option, offset: Option, ) -> Result, AppCommandError> { let limit = limit.unwrap_or(50); let offset = offset.unwrap_or(0); let rows = chat_channel_message_log_service::list_by_channel(&db.conn, channel_id, limit, offset) .await .map_err(AppCommandError::from)?; Ok(rows.into_iter().map(ChatChannelMessageLogInfo::from).collect()) } const COMMAND_PREFIX_KEY: &str = "chat_command_prefix"; const DEFAULT_COMMAND_PREFIX: &str = "/"; pub async fn get_chat_command_prefix_core( db: &AppDatabase, ) -> Result { let val = crate::db::service::app_metadata_service::get_value(&db.conn, COMMAND_PREFIX_KEY) .await .map_err(AppCommandError::from)?; Ok(val.unwrap_or_else(|| DEFAULT_COMMAND_PREFIX.to_string())) } pub async fn set_chat_command_prefix_core( db: &AppDatabase, prefix: String, ) -> Result<(), AppCommandError> { let trimmed = prefix.trim(); if trimmed.is_empty() || trimmed.len() > 3 || trimmed.chars().any(|c| c.is_alphanumeric()) { return Err(AppCommandError::invalid_input( "Prefix must be 1-3 non-alphanumeric characters", )); } crate::db::service::app_metadata_service::upsert_value(&db.conn, COMMAND_PREFIX_KEY, trimmed) .await .map_err(AppCommandError::from)?; Ok(()) } const MESSAGE_LANGUAGE_KEY: &str = "chat_message_language"; pub async fn get_chat_message_language_core( db: &AppDatabase, ) -> Result { let val = crate::db::service::app_metadata_service::get_value(&db.conn, MESSAGE_LANGUAGE_KEY) .await .map_err(AppCommandError::from)?; Ok(val.unwrap_or_else(|| "en".to_string())) } pub async fn set_chat_message_language_core( db: &AppDatabase, language: String, ) -> Result<(), AppCommandError> { // Validate language code let valid = [ "en", "zh-cn", "zh-tw", "ja", "ko", "es", "de", "fr", "pt", "ar", ]; let lang_lower = language.to_lowercase(); if !valid.contains(&lang_lower.as_str()) { return Err(AppCommandError::invalid_input(format!( "Unsupported language: {language}. Supported: {}", valid.join(", ") ))); } crate::db::service::app_metadata_service::upsert_value( &db.conn, MESSAGE_LANGUAGE_KEY, &lang_lower, ) .await .map_err(AppCommandError::from)?; Ok(()) } const EVENT_FILTER_KEY: &str = "chat_event_filter"; pub async fn get_chat_event_filter_core( db: &AppDatabase, ) -> Result>, AppCommandError> { let val = crate::db::service::app_metadata_service::get_value(&db.conn, EVENT_FILTER_KEY) .await .map_err(AppCommandError::from)?; match val { Some(json) => { // Parse as Option> to correctly handle stored "null" let filter: Option> = serde_json::from_str(&json) .map_err(|e| AppCommandError::invalid_input(e.to_string()))?; Ok(filter) } None => Ok(None), } } pub async fn set_chat_event_filter_core( db: &AppDatabase, filter: Option>, ) -> Result<(), AppCommandError> { match filter { Some(arr) => { let json = serde_json::to_string(&arr) .map_err(|e| AppCommandError::invalid_input(e.to_string()))?; crate::db::service::app_metadata_service::upsert_value( &db.conn, EVENT_FILTER_KEY, &json, ) .await .map_err(AppCommandError::from)?; } None => { // null means all events enabled — remove the key crate::db::service::app_metadata_service::upsert_value( &db.conn, EVENT_FILTER_KEY, "null", ) .await .map_err(AppCommandError::from)?; } } Ok(()) } // --------------------------------------------------------------------------- // WeChat QR code auth // --------------------------------------------------------------------------- pub async fn weixin_get_qrcode_core() -> Result { crate::chat_channel::backends::weixin::weixin_get_qrcode() .await .map_err(AppCommandError::from) } pub async fn weixin_check_qrcode_core( db: &AppDatabase, channel_id: i32, qrcode: &str, ) -> Result { let result = crate::chat_channel::backends::weixin::weixin_check_qrcode(qrcode) .await .map_err(AppCommandError::from)?; // On confirmed: save token + update config with base_url if result.status == "confirmed" { eprintln!( "[Weixin] QR confirmed for channel {channel_id}, bot_token={}, base_url={}", result.bot_token.as_deref().map(|t| if t.len() > 8 { &t[..8] } else { t }).unwrap_or("None"), result.base_url.as_deref().unwrap_or("None"), ); if let Some(ref token) = result.bot_token { save_chat_channel_token_core(channel_id, token)?; eprintln!("[Weixin] Token saved for channel {channel_id}"); } else { eprintln!("[Weixin] WARNING: No bot_token in confirmed response for channel {channel_id}"); } if let Some(ref base_url) = result.base_url { let config_json = serde_json::json!({ "base_url": base_url }).to_string(); update_chat_channel_core( db, channel_id, None, None, Some(config_json), None, None, None, ) .await?; eprintln!("[Weixin] Config updated with base_url for channel {channel_id}"); } } // Return only the status — never expose bot_token to the frontend Ok(WeixinQrcodeStatusPublic { status: result.status, }) } // --------------------------------------------------------------------------- // Tauri commands (use tauri::State for injection) // --------------------------------------------------------------------------- #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn list_chat_channels( db: tauri::State<'_, AppDatabase>, ) -> Result, AppCommandError> { list_chat_channels_core(&db).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn create_chat_channel( db: tauri::State<'_, AppDatabase>, name: String, channel_type: String, config_json: String, enabled: bool, daily_report_enabled: bool, daily_report_time: Option, ) -> Result { create_chat_channel_core(&db, name, channel_type, config_json, enabled, daily_report_enabled, daily_report_time).await } #[allow(clippy::too_many_arguments)] #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn update_chat_channel( db: tauri::State<'_, AppDatabase>, id: i32, name: Option, enabled: Option, config_json: Option, event_filter_json: Option>, daily_report_enabled: Option, daily_report_time: Option>, ) -> Result { update_chat_channel_core(&db, id, name, enabled, config_json, event_filter_json, daily_report_enabled, daily_report_time).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn delete_chat_channel( db: tauri::State<'_, AppDatabase>, manager: tauri::State<'_, ChatChannelManager>, id: i32, ) -> Result<(), AppCommandError> { delete_chat_channel_core(&db, &manager, id).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn save_chat_channel_token(channel_id: i32, token: String) -> Result<(), AppCommandError> { save_chat_channel_token_core(channel_id, &token) } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_chat_channel_has_token(channel_id: i32) -> Result { get_chat_channel_has_token_core(channel_id) } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn delete_chat_channel_token(channel_id: i32) -> Result<(), AppCommandError> { delete_chat_channel_token_core(channel_id) } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn connect_chat_channel( db: tauri::State<'_, AppDatabase>, manager: tauri::State<'_, ChatChannelManager>, id: i32, ) -> Result<(), AppCommandError> { connect_chat_channel_core(&db, &manager, id).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn disconnect_chat_channel( manager: tauri::State<'_, ChatChannelManager>, id: i32, ) -> Result<(), AppCommandError> { disconnect_chat_channel_core(&manager, id).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn test_chat_channel( db: tauri::State<'_, AppDatabase>, id: i32, ) -> Result<(), AppCommandError> { test_chat_channel_core(&db, id).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_chat_channel_status( manager: tauri::State<'_, ChatChannelManager>, ) -> Result, AppCommandError> { get_chat_channel_status_core(&manager).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn list_chat_channel_messages( db: tauri::State<'_, AppDatabase>, channel_id: i32, limit: Option, offset: Option, ) -> Result, AppCommandError> { list_chat_channel_messages_core(&db, channel_id, limit, offset).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_chat_command_prefix( db: tauri::State<'_, AppDatabase>, ) -> Result { get_chat_command_prefix_core(&db).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn set_chat_command_prefix( db: tauri::State<'_, AppDatabase>, prefix: String, ) -> Result<(), AppCommandError> { set_chat_command_prefix_core(&db, prefix).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_chat_event_filter( db: tauri::State<'_, AppDatabase>, ) -> Result>, AppCommandError> { get_chat_event_filter_core(&db).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn set_chat_event_filter( db: tauri::State<'_, AppDatabase>, filter: Option>, ) -> Result<(), AppCommandError> { set_chat_event_filter_core(&db, filter).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_chat_message_language( db: tauri::State<'_, AppDatabase>, ) -> Result { get_chat_message_language_core(&db).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn set_chat_message_language( db: tauri::State<'_, AppDatabase>, language: String, ) -> Result<(), AppCommandError> { set_chat_message_language_core(&db, language).await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn weixin_get_qrcode() -> Result { weixin_get_qrcode_core().await } #[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn weixin_check_qrcode( db: tauri::State<'_, AppDatabase>, channel_id: i32, qrcode: String, ) -> Result { weixin_check_qrcode_core(&db, channel_id, &qrcode).await }