diff --git a/src/attendance/keyword_services.rs b/src/attendance/keyword_services.rs index be62ddc9f..8ebfdf709 100644 --- a/src/attendance/keyword_services.rs +++ b/src/attendance/keyword_services.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; -use chrono::{DateTime, Duration, Local, NaiveTime, Utc}; +use chrono::{DateTime, Duration, Local, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; diff --git a/src/attendance/llm_assist.rs b/src/attendance/llm_assist.rs index 593d22dc1..10b424157 100644 --- a/src/attendance/llm_assist.rs +++ b/src/attendance/llm_assist.rs @@ -1,40 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - use crate::core::config::ConfigManager; use crate::shared::models::UserSession; use crate::shared::state::AppState; @@ -46,18 +9,14 @@ use axum::{ }; use chrono::Utc; use diesel::prelude::*; -use log::{debug, error, info, warn}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; - - - #[derive(Debug, Clone, Default)] pub struct LlmAssistConfig { - pub tips_enabled: bool, pub polish_enabled: bool, @@ -74,7 +33,6 @@ pub struct LlmAssistConfig { } impl LlmAssistConfig { - pub fn from_config(bot_id: Uuid, work_path: &str) -> Self { let config_path = PathBuf::from(work_path) .join(format!("{}.gbai", bot_id)) @@ -143,7 +101,6 @@ impl LlmAssistConfig { config } - pub fn any_enabled(&self) -> bool { self.tips_enabled || self.polish_enabled @@ -153,9 +110,6 @@ impl LlmAssistConfig { } } - - - #[derive(Debug, Deserialize)] pub struct TipRequest { pub session_id: Uuid, @@ -165,7 +119,6 @@ pub struct TipRequest { pub history: Vec, } - #[derive(Debug, Deserialize)] pub struct PolishRequest { pub session_id: Uuid, @@ -179,7 +132,6 @@ fn default_tone() -> String { "professional".to_string() } - #[derive(Debug, Deserialize)] pub struct SmartRepliesRequest { pub session_id: Uuid, @@ -187,13 +139,11 @@ pub struct SmartRepliesRequest { pub history: Vec, } - #[derive(Debug, Deserialize)] pub struct SummaryRequest { pub session_id: Uuid, } - #[derive(Debug, Deserialize)] pub struct SentimentRequest { pub session_id: Uuid, @@ -202,7 +152,6 @@ pub struct SentimentRequest { pub history: Vec, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConversationMessage { pub role: String, @@ -210,7 +159,6 @@ pub struct ConversationMessage { pub timestamp: Option, } - #[derive(Debug, Serialize)] pub struct TipResponse { pub success: bool, @@ -219,7 +167,6 @@ pub struct TipResponse { pub error: Option, } - #[derive(Debug, Clone, Serialize)] pub struct AttendantTip { pub tip_type: TipType, @@ -228,11 +175,9 @@ pub struct AttendantTip { pub priority: i32, } - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TipType { - Intent, Action, @@ -246,7 +191,6 @@ pub enum TipType { General, } - #[derive(Debug, Serialize)] pub struct PolishResponse { pub success: bool, @@ -257,7 +201,6 @@ pub struct PolishResponse { pub error: Option, } - #[derive(Debug, Serialize)] pub struct SmartRepliesResponse { pub success: bool, @@ -266,7 +209,6 @@ pub struct SmartRepliesResponse { pub error: Option, } - #[derive(Debug, Clone, Serialize)] pub struct SmartReply { pub text: String, @@ -275,7 +217,6 @@ pub struct SmartReply { pub category: String, } - #[derive(Debug, Serialize)] pub struct SummaryResponse { pub success: bool, @@ -284,7 +225,6 @@ pub struct SummaryResponse { pub error: Option, } - #[derive(Debug, Clone, Serialize, Default)] pub struct ConversationSummary { pub brief: String, @@ -297,7 +237,6 @@ pub struct ConversationSummary { pub duration_minutes: i32, } - #[derive(Debug, Serialize)] pub struct SentimentResponse { pub success: bool, @@ -306,7 +245,6 @@ pub struct SentimentResponse { pub error: Option, } - #[derive(Debug, Clone, Serialize, Default)] pub struct SentimentAnalysis { pub overall: String, @@ -317,16 +255,12 @@ pub struct SentimentAnalysis { pub emoji: String, } - #[derive(Debug, Clone, Serialize)] pub struct Emotion { pub name: String, pub intensity: f32, } - - - async fn execute_llm_with_context( state: &Arc, bot_id: Uuid, @@ -351,7 +285,6 @@ async fn execute_llm_with_context( .unwrap_or_default() }); - let messages = serde_json::json!([ { "role": "system", @@ -368,29 +301,24 @@ async fn execute_llm_with_context( .generate(user_prompt, &messages, &model, &key) .await?; - let handler = crate::llm::llm_models::get_handler(&model); let processed = handler.process_content(&response); Ok(processed) } - fn get_bot_system_prompt(bot_id: Uuid, work_path: &str) -> String { - let config = LlmAssistConfig::from_config(bot_id, work_path); if let Some(prompt) = config.bot_system_prompt { return prompt; } - let start_bas_path = PathBuf::from(work_path) .join(format!("{}.gbai", bot_id)) .join(format!("{}.gbdialog", bot_id)) .join("start.bas"); if let Ok(content) = std::fs::read_to_string(&start_bas_path) { - let mut description_lines = Vec::new(); for line in content.lines() { let trimmed = line.trim(); @@ -406,21 +334,15 @@ fn get_bot_system_prompt(bot_id: Uuid, work_path: &str) -> String { } } - "You are a professional customer service assistant. Be helpful, empathetic, and solution-oriented. Maintain a friendly but professional tone.".to_string() } - - - - pub async fn generate_tips( State(state): State>, Json(request): Json, ) -> (StatusCode, Json) { info!("Generating tips for session {}", request.session_id); - let session_result = get_session(&state, request.session_id).await; let session = match session_result { Ok(s) => s, @@ -436,7 +358,6 @@ pub async fn generate_tips( } }; - let work_path = std::env::var("WORK_PATH").unwrap_or_else(|_| "./work".to_string()); let config = LlmAssistConfig::from_config(session.bot_id, &work_path); @@ -451,7 +372,6 @@ pub async fn generate_tips( ); } - let history_context = request .history .iter() @@ -497,7 +417,6 @@ Provide tips for the attendant."#, match execute_llm_with_context(&state, session.bot_id, &system_prompt, &user_prompt).await { Ok(response) => { - let tips = parse_tips_response(&response); ( StatusCode::OK, @@ -523,8 +442,6 @@ Provide tips for the attendant."#, } } - - pub async fn polish_message( State(state): State>, Json(request): Json, @@ -621,8 +538,6 @@ Respond in JSON format: } } - - pub async fn generate_smart_replies( State(state): State>, Json(request): Json, @@ -677,7 +592,7 @@ The service has this personality: {} Generate exactly 3 reply suggestions that: 1. Are contextually appropriate 2. Sound natural and human (not robotic) -3. Vary in approach (one empathetic, one solution-focused, one follow-up) +3. Vary in approach (one empathetic, one solution-focused, one follow_up) 4. Are ready to send (no placeholders like [name]) Respond in JSON format: @@ -692,10 +607,10 @@ Respond in JSON format: ); let user_prompt = format!( - r#"Conversation: + r"Conversation: {} -Generate 3 reply options for the attendant."#, +Generate 3 reply options for the attendant.", history_context ); @@ -725,8 +640,6 @@ Generate 3 reply options for the attendant."#, } } - - pub async fn generate_summary( State(state): State>, Path(session_id): Path, @@ -762,7 +675,6 @@ pub async fn generate_summary( ); } - let history = load_conversation_history(&state, session_id).await; if history.is_empty() { @@ -806,9 +718,9 @@ Respond in JSON format: ); let user_prompt = format!( - r#"Summarize this conversation: + r"Summarize this conversation: -{}"#, +{}", history_text ); @@ -817,7 +729,6 @@ Respond in JSON format: let mut summary = parse_summary_response(&response); summary.message_count = history.len() as i32; - if let (Some(first_ts), Some(last_ts)) = ( history.first().and_then(|m| m.timestamp.as_ref()), history.last().and_then(|m| m.timestamp.as_ref()), @@ -857,8 +768,6 @@ Respond in JSON format: } } - - pub async fn analyze_sentiment( State(state): State>, Json(request): Json, @@ -884,7 +793,6 @@ pub async fn analyze_sentiment( let config = LlmAssistConfig::from_config(session.bot_id, &work_path); if !config.sentiment_enabled { - let sentiment = analyze_sentiment_keywords(&request.message); return ( StatusCode::OK, @@ -959,8 +867,6 @@ Analyze the customer's sentiment."#, } } - - pub async fn get_llm_config( State(_state): State>, Path(bot_id): Path, @@ -981,9 +887,6 @@ pub async fn get_llm_config( ) } - - - pub async fn process_attendant_command( state: &Arc, attendant_phone: &str, @@ -1020,7 +923,6 @@ pub async fn process_attendant_command( } async fn handle_queue_command(state: &Arc) -> Result { - let conn = state.conn.clone(); let result = tokio::task::spawn_blocking(move || { let mut db_conn = conn.get().map_err(|e| e.to_string())?; @@ -1174,8 +1076,6 @@ async fn handle_status_command( } }; - - let conn = state.conn.clone(); let phone = attendant_phone.to_string(); let status_val = status_value.to_string(); @@ -1185,8 +1085,6 @@ async fn handle_status_command( use crate::shared::models::schema::user_sessions; - - let sessions: Vec = user_sessions::table .filter( user_sessions::context_data @@ -1243,7 +1141,6 @@ async fn handle_transfer_command( let target = args.join(" "); let target_clean = target.trim_start_matches('@').to_string(); - let conn = state.conn.clone(); let target_attendant = target_clean.clone(); @@ -1252,13 +1149,11 @@ async fn handle_transfer_command( use crate::shared::models::schema::user_sessions; - let session: UserSession = user_sessions::table .find(session_id) .first(&mut db_conn) .map_err(|e| format!("Session not found: {}", e))?; - let mut ctx = session.context_data.clone(); let previous_attendant = ctx .get("assigned_to_phone") @@ -1271,11 +1166,9 @@ async fn handle_transfer_command( ctx["transferred_at"] = serde_json::json!(Utc::now().to_rfc3339()); ctx["status"] = serde_json::json!("pending_transfer"); - ctx["assigned_to_phone"] = serde_json::Value::Null; ctx["assigned_to"] = serde_json::Value::Null; - ctx["needs_human"] = serde_json::json!(true); diesel::update(user_sessions::table.filter(user_sessions::id.eq(session_id))) @@ -1347,7 +1240,6 @@ async fn handle_tips_command( ) -> Result { let session_id = current_session.ok_or("No active conversation. Use /take first.")?; - let history = load_conversation_history(state, session_id).await; if history.is_empty() { @@ -1369,7 +1261,6 @@ async fn handle_tips_command( history, }; - let (_, Json(tip_response)) = generate_tips(State(state.clone()), Json(request)).await; if tip_response.tips.is_empty() { @@ -1446,7 +1337,8 @@ async fn handle_replies_command( history, }; - let (_, Json(replies_response)) = generate_smart_replies(State(state.clone()), Json(request)).await; + let (_, Json(replies_response)) = + generate_smart_replies(State(state.clone()), Json(request)).await; if replies_response.replies.is_empty() { return Ok(" No reply suggestions available.".to_string()); @@ -1474,7 +1366,8 @@ async fn handle_summary_command( ) -> Result { let session_id = current_session.ok_or("No active conversation")?; - let (_, Json(summary_response)) = generate_summary(State(state.clone()), Path(session_id)).await; + let (_, Json(summary_response)) = + generate_summary(State(state.clone()), Path(session_id)).await; if !summary_response.success { return Err(summary_response @@ -1527,7 +1420,7 @@ async fn handle_summary_command( } fn get_help_text() -> String { - r#" *Attendant Commands* + r#"*Attendant Commands* *Queue Management:* `/queue` - View waiting conversations @@ -1549,9 +1442,6 @@ _Portuguese: /fila, /pegar, /transferir, /resolver, /dicas, /polir, /respostas, .to_string() } - - - async fn get_session(state: &Arc, session_id: Uuid) -> Result { let conn = state.conn.clone(); @@ -1569,7 +1459,6 @@ async fn get_session(state: &Arc, session_id: Uuid) -> Result, session_id: Uuid, @@ -1616,9 +1505,7 @@ async fn load_conversation_history( result } - fn parse_tips_response(response: &str) -> Vec { - let json_str = extract_json(response); if let Ok(parsed) = serde_json::from_str::(&json_str) { @@ -1653,7 +1540,6 @@ fn parse_tips_response(response: &str) -> Vec { } } - if !response.trim().is_empty() { vec![AttendantTip { tip_type: TipType::General, @@ -1666,7 +1552,6 @@ fn parse_tips_response(response: &str) -> Vec { } } - fn parse_polish_response(response: &str, original: &str) -> (String, Vec) { let json_str = extract_json(response); @@ -1690,14 +1575,12 @@ fn parse_polish_response(response: &str, original: &str) -> (String, Vec return (polished, changes); } - ( response.trim().to_string(), vec!["Message improved".to_string()], ) } - fn parse_smart_replies_response(response: &str) -> Vec { let json_str = extract_json(response); @@ -1731,7 +1614,6 @@ fn parse_smart_replies_response(response: &str) -> Vec { generate_fallback_replies() } - fn parse_summary_response(response: &str) -> ConversationSummary { let json_str = extract_json(response); @@ -1789,7 +1671,6 @@ fn parse_summary_response(response: &str) -> ConversationSummary { } } - fn parse_sentiment_response(response: &str) -> SentimentAnalysis { let json_str = extract_json(response); @@ -1839,9 +1720,7 @@ fn parse_sentiment_response(response: &str) -> SentimentAnalysis { SentimentAnalysis::default() } - fn extract_json(response: &str) -> String { - if let Some(start) = response.find('{') { if let Some(end) = response.rfind('}') { if end > start { @@ -1850,7 +1729,6 @@ fn extract_json(response: &str) -> String { } } - if let Some(start) = response.find('[') { if let Some(end) = response.rfind(']') { if end > start { @@ -1862,12 +1740,10 @@ fn extract_json(response: &str) -> String { response.to_string() } - fn generate_fallback_tips(message: &str) -> Vec { let msg_lower = message.to_lowercase(); let mut tips = Vec::new(); - if msg_lower.contains("urgent") || msg_lower.contains("asap") || msg_lower.contains("immediately") @@ -1881,7 +1757,6 @@ fn generate_fallback_tips(message: &str) -> Vec { }); } - if msg_lower.contains("frustrated") || msg_lower.contains("angry") || msg_lower.contains("ridiculous") @@ -1895,7 +1770,6 @@ fn generate_fallback_tips(message: &str) -> Vec { }); } - if message.contains('?') { tips.push(AttendantTip { tip_type: TipType::Intent, @@ -1905,7 +1779,6 @@ fn generate_fallback_tips(message: &str) -> Vec { }); } - if msg_lower.contains("problem") || msg_lower.contains("issue") || msg_lower.contains("not working") @@ -1919,7 +1792,6 @@ fn generate_fallback_tips(message: &str) -> Vec { }); } - if msg_lower.contains("thank") || msg_lower.contains("great") || msg_lower.contains("perfect") @@ -1934,7 +1806,6 @@ fn generate_fallback_tips(message: &str) -> Vec { }); } - if tips.is_empty() { tips.push(AttendantTip { tip_type: TipType::General, @@ -1947,7 +1818,6 @@ fn generate_fallback_tips(message: &str) -> Vec { tips } - fn generate_fallback_replies() -> Vec { vec![ SmartReply { @@ -1971,7 +1841,6 @@ fn generate_fallback_replies() -> Vec { ] } - fn analyze_sentiment_keywords(message: &str) -> SentimentAnalysis { let msg_lower = message.to_lowercase(); @@ -2097,5 +1966,3 @@ fn analyze_sentiment_keywords(message: &str) -> SentimentAnalysis { emoji: emoji.to_string(), } } - - diff --git a/src/attendance/mod.rs b/src/attendance/mod.rs index d8221341d..802f22c57 100644 --- a/src/attendance/mod.rs +++ b/src/attendance/mod.rs @@ -1,40 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - pub mod drive; pub mod keyword_services; pub mod llm_assist; pub mod queue; - pub use drive::{AttendanceDriveConfig, AttendanceDriveService, RecordMetadata, SyncResult}; pub use keyword_services::{ AttendanceCommand, AttendanceRecord, AttendanceResponse, AttendanceService, KeywordConfig, @@ -58,14 +26,13 @@ use crate::shared::state::{AppState, AttendantNotification}; use axum::{ extract::{ ws::{Message, WebSocket, WebSocketUpgrade}, - Path, Query, State, + Query, State, }, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; -use botlib::MessageType; use chrono::Utc; use diesel::prelude::*; use futures::{SinkExt, StreamExt}; @@ -76,10 +43,8 @@ use std::sync::Arc; use tokio::sync::broadcast; use uuid::Uuid; - pub fn configure_attendance_routes() -> Router> { Router::new() - .route("/api/attendance/queue", get(queue::list_queue)) .route("/api/attendance/attendants", get(queue::list_attendants)) .route("/api/attendance/assign", post(queue::assign_conversation)) @@ -92,11 +57,8 @@ pub fn configure_attendance_routes() -> Router> { post(queue::resolve_conversation), ) .route("/api/attendance/insights", get(queue::get_insights)) - .route("/api/attendance/respond", post(attendant_respond)) - .route("/ws/attendant", get(attendant_websocket_handler)) - .route("/api/attendance/llm/tips", post(llm_assist::generate_tips)) .route( "/api/attendance/llm/polish", @@ -120,7 +82,6 @@ pub fn configure_attendance_routes() -> Router> { ) } - #[derive(Debug, Deserialize)] pub struct AttendantRespondRequest { pub session_id: String, @@ -128,7 +89,6 @@ pub struct AttendantRespondRequest { pub attendant_id: String, } - #[derive(Debug, Serialize)] pub struct AttendantRespondResponse { pub success: bool, @@ -137,7 +97,6 @@ pub struct AttendantRespondResponse { pub error: Option, } - pub async fn attendant_respond( State(state): State>, Json(request): Json, @@ -161,7 +120,6 @@ pub async fn attendant_respond( } }; - let conn = state.conn.clone(); let session_result = tokio::task::spawn_blocking(move || { let mut db_conn = conn.get().ok()?; @@ -189,26 +147,22 @@ pub async fn attendant_respond( } }; - let channel = session .context_data .get("channel") .and_then(|v| v.as_str()) .unwrap_or("web"); - let recipient = session .context_data .get("phone") .and_then(|v| v.as_str()) .unwrap_or(""); - if let Err(e) = save_message_to_history(&state, &session, &request.message, "attendant").await { error!("Failed to save attendant message: {}", e); } - match channel { "whatsapp" => { if recipient.is_empty() { @@ -240,7 +194,6 @@ pub async fn attendant_respond( match adapter.send_message(response).await { Ok(_) => { - broadcast_attendant_action(&state, &session, &request, "attendant_response") .await; @@ -264,7 +217,6 @@ pub async fn attendant_respond( } } "web" | _ => { - let sent = if let Some(tx) = state .response_channels .lock() @@ -282,15 +234,14 @@ pub async fn attendant_respond( is_complete: true, suggestions: vec![], context_name: None, - context_length: 0, - context_max_length: 0, + context_length: 0, + context_max_length: 0, }; tx.send(response).await.is_ok() } else { false }; - broadcast_attendant_action(&state, &session, &request, "attendant_response").await; if sent { @@ -303,7 +254,6 @@ pub async fn attendant_respond( }), ) } else { - ( StatusCode::OK, Json(AttendantRespondResponse { @@ -317,7 +267,6 @@ pub async fn attendant_respond( } } - async fn save_message_to_history( state: &Arc, session: &UserSession, @@ -356,7 +305,6 @@ async fn save_message_to_history( Ok(()) } - async fn broadcast_attendant_action( state: &Arc, session: &UserSession, @@ -396,7 +344,6 @@ async fn broadcast_attendant_action( } } - pub async fn attendant_websocket_handler( ws: WebSocketUpgrade, State(state): State>, @@ -422,13 +369,11 @@ pub async fn attendant_websocket_handler( .into_response() } - async fn handle_attendant_websocket(socket: WebSocket, state: Arc, attendant_id: String) { let (mut sender, mut receiver) = socket.split(); info!("Attendant WebSocket connected: {}", attendant_id); - let welcome = serde_json::json!({ "type": "connected", "attendant_id": attendant_id, @@ -447,7 +392,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att } } - let mut broadcast_rx = if let Some(broadcast_tx) = state.attendant_broadcast.as_ref() { broadcast_tx.subscribe() } else { @@ -455,14 +399,11 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att return; }; - let attendant_id_clone = attendant_id.clone(); let mut send_task = tokio::spawn(async move { loop { match broadcast_rx.recv().await { Ok(notification) => { - - let should_send = notification.assigned_to.is_none() || notification.assigned_to.as_ref() == Some(&attendant_id_clone); @@ -493,7 +434,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att } }); - let state_clone = state.clone(); let attendant_id_for_recv = attendant_id.clone(); let mut recv_task = tokio::spawn(async move { @@ -505,7 +445,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att attendant_id_for_recv, text ); - if let Ok(parsed) = serde_json::from_str::(&text) { handle_attendant_message(&state_clone, &attendant_id_for_recv, parsed) .await; @@ -513,7 +452,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att } Message::Ping(data) => { debug!("Received ping from attendant {}", attendant_id_for_recv); - } Message::Close(_) => { info!( @@ -527,7 +465,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att } }); - tokio::select! { _ = (&mut send_task) => { recv_task.abort(); @@ -540,7 +477,6 @@ async fn handle_attendant_websocket(socket: WebSocket, state: Arc, att info!("Attendant WebSocket disconnected: {}", attendant_id); } - async fn handle_attendant_message( state: &Arc, attendant_id: &str, @@ -553,24 +489,19 @@ async fn handle_attendant_message( match msg_type { "status_update" => { - if let Some(status) = message.get("status").and_then(|v| v.as_str()) { info!("Attendant {} status update: {}", attendant_id, status); - } } "typing" => { - if let Some(session_id) = message.get("session_id").and_then(|v| v.as_str()) { debug!( "Attendant {} typing in session {}", attendant_id, session_id ); - } } "read" => { - if let Some(session_id) = message.get("session_id").and_then(|v| v.as_str()) { debug!( "Attendant {} marked session {} as read", @@ -579,7 +510,6 @@ async fn handle_attendant_message( } } "respond" => { - if let (Some(session_id), Some(content)) = ( message.get("session_id").and_then(|v| v.as_str()), message.get("content").and_then(|v| v.as_str()), @@ -589,14 +519,12 @@ async fn handle_attendant_message( attendant_id, session_id ); - let request = AttendantRespondRequest { session_id: session_id.to_string(), message: content.to_string(), attendant_id: attendant_id.to_string(), }; - if let Ok(uuid) = Uuid::parse_str(session_id) { let conn = state.conn.clone(); if let Some(session) = tokio::task::spawn_blocking(move || { @@ -611,11 +539,9 @@ async fn handle_attendant_message( .ok() .flatten() { - let _ = save_message_to_history(state, &session, content, "attendant").await; - let channel = session .context_data .get("channel") @@ -639,14 +565,13 @@ async fn handle_attendant_message( is_complete: true, suggestions: vec![], context_name: None, - context_length: 0, - context_max_length: 0, + context_length: 0, + context_max_length: 0, }; let _ = adapter.send_message(response).await; } } - broadcast_attendant_action(state, &session, &request, "attendant_response") .await; } diff --git a/src/basic/keywords/agent_reflection.rs b/src/basic/keywords/agent_reflection.rs index 814d198cf..973d915da 100644 --- a/src/basic/keywords/agent_reflection.rs +++ b/src/basic/keywords/agent_reflection.rs @@ -1,22 +1,3 @@ - - - - - - - - - - - - - - - - - - - use crate::shared::models::UserSession; use crate::shared::state::AppState; use diesel::prelude::*; @@ -27,10 +8,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ReflectionType { - ConversationQuality, ResponseAccuracy, @@ -66,7 +45,6 @@ impl From<&str> for ReflectionType { } impl ReflectionType { - pub fn prompt_template(&self) -> String { match self { ReflectionType::ConversationQuality => { @@ -190,10 +168,8 @@ Provide your analysis in JSON format: } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReflectionConfig { - pub enabled: bool, pub interval: u32, @@ -224,7 +200,6 @@ impl Default for ReflectionConfig { } impl ReflectionConfig { - pub fn from_bot_config(state: &AppState, bot_id: Uuid) -> Self { let mut config = Self::default(); @@ -278,10 +253,8 @@ impl ReflectionConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReflectionResult { - pub id: Uuid, pub bot_id: Uuid, @@ -325,7 +298,6 @@ impl ReflectionResult { } } - pub fn from_llm_response( bot_id: Uuid, session_id: Uuid, @@ -337,16 +309,13 @@ impl ReflectionResult { result.raw_response = response.to_string(); result.messages_analyzed = messages_analyzed; - if let Ok(json) = serde_json::from_str::(response) { - result.score = json .get("score") .or_else(|| json.get("overall_score")) .and_then(|v| v.as_f64()) .unwrap_or(5.0) as f32; - if let Some(insights) = json.get("key_insights").or_else(|| json.get("insights")) { if let Some(arr) = insights.as_array() { result.insights = arr @@ -356,7 +325,6 @@ impl ReflectionResult { } } - if let Some(improvements) = json .get("improvements") .or_else(|| json.get("critical_improvements")) @@ -370,7 +338,6 @@ impl ReflectionResult { } } - if let Some(patterns) = json .get("positive_patterns") .or_else(|| json.get("strengths")) @@ -384,7 +351,6 @@ impl ReflectionResult { } } - if let Some(concerns) = json .get("weaknesses") .or_else(|| json.get("concerns")) @@ -399,7 +365,6 @@ impl ReflectionResult { } } } else { - warn!("Reflection response was not valid JSON, extracting from plain text"); result.insights = extract_insights_from_text(response); result.score = 5.0; @@ -408,12 +373,10 @@ impl ReflectionResult { result } - pub fn needs_improvement(&self, threshold: f32) -> bool { self.score < threshold } - pub fn summary(&self) -> String { format!( "Reflection Score: {:.1}/10\n\ @@ -430,11 +393,9 @@ impl ReflectionResult { } } - -fn extract_insights_from_text(text: &str) -> Vec { +pub fn extract_insights_from_text(text: &str) -> Vec { let mut insights = Vec::new(); - for line in text.lines() { let trimmed = line.trim(); if trimmed.starts_with(|c: char| c.is_ascii_digit()) @@ -453,7 +414,6 @@ fn extract_insights_from_text(text: &str) -> Vec { } } - if insights.is_empty() { for sentence in text.split(|c| c == '.' || c == '!' || c == '?') { let trimmed = sentence.trim(); @@ -467,7 +427,6 @@ fn extract_insights_from_text(text: &str) -> Vec { insights } - pub struct ReflectionEngine { state: Arc, config: ReflectionConfig, @@ -501,7 +460,6 @@ impl ReflectionEngine { } } - pub async fn reflect( &self, session_id: Uuid, @@ -511,7 +469,6 @@ impl ReflectionEngine { return Err("Reflection is not enabled for this bot".to_string()); } - let history = self.get_recent_history(session_id, 20).await?; if history.is_empty() { @@ -520,13 +477,10 @@ impl ReflectionEngine { let messages_count = history.len(); - let prompt = self.build_reflection_prompt(&reflection_type, &history)?; - let response = self.call_llm_for_reflection(&prompt).await?; - let result = ReflectionResult::from_llm_response( self.bot_id, session_id, @@ -535,10 +489,8 @@ impl ReflectionEngine { messages_count, ); - self.store_reflection(&result).await?; - if self.config.auto_apply && result.needs_improvement(self.config.improvement_threshold) { self.apply_improvements(&result).await?; } @@ -551,7 +503,6 @@ impl ReflectionEngine { Ok(result) } - async fn get_recent_history( &self, session_id: Uuid, @@ -595,7 +546,6 @@ impl ReflectionEngine { Ok(history) } - fn build_reflection_prompt( &self, reflection_type: &ReflectionType, @@ -607,7 +557,6 @@ impl ReflectionEngine { reflection_type.prompt_template() }; - let conversation = history .iter() .map(|m| { @@ -629,9 +578,7 @@ impl ReflectionEngine { Ok(prompt) } - async fn call_llm_for_reflection(&self, prompt: &str) -> Result { - let (llm_url, llm_model, llm_key) = self.get_llm_config().await?; let client = reqwest::Client::new(); @@ -680,7 +627,6 @@ impl ReflectionEngine { Ok(content) } - async fn get_llm_config(&self) -> Result<(String, String, String), String> { let mut conn = self .state @@ -720,7 +666,6 @@ impl ReflectionEngine { Ok((llm_url, llm_model, llm_key)) } - async fn store_reflection(&self, result: &ReflectionResult) -> Result<(), String> { let mut conn = self .state @@ -759,9 +704,7 @@ impl ReflectionEngine { Ok(()) } - async fn apply_improvements(&self, result: &ReflectionResult) -> Result<(), String> { - let mut conn = self .state .conn @@ -796,7 +739,6 @@ impl ReflectionEngine { Ok(()) } - pub async fn get_insights(&self, limit: usize) -> Result, String> { let mut conn = self .state @@ -867,13 +809,11 @@ impl ReflectionEngine { Ok(results) } - pub async fn should_reflect(&self, session_id: Uuid) -> bool { if !self.config.enabled { return false; } - if let Ok(mut conn) = self.state.conn.get() { #[derive(QueryableByName)] struct CountRow { @@ -897,7 +837,6 @@ impl ReflectionEngine { } } - #[derive(Debug, Clone)] struct ConversationMessage { pub role: String, @@ -905,14 +844,12 @@ struct ConversationMessage { pub timestamp: chrono::DateTime, } - pub fn register_reflection_keywords(state: Arc, user: UserSession, engine: &mut Engine) { set_bot_reflection_keyword(state.clone(), user.clone(), engine); reflect_on_keyword(state.clone(), user.clone(), engine); get_reflection_insights_keyword(state.clone(), user.clone(), engine); } - pub fn set_bot_reflection_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -964,7 +901,6 @@ pub fn set_bot_reflection_keyword(state: Arc, user: UserSession, engin .expect("Failed to register SET BOT REFLECTION syntax"); } - pub fn reflect_on_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -1016,7 +952,6 @@ pub fn reflect_on_keyword(state: Arc, user: UserSession, engine: &mut .expect("Failed to register REFLECT ON syntax"); } - pub fn get_reflection_insights_keyword( state: Arc, user: UserSession, @@ -1045,7 +980,6 @@ pub fn get_reflection_insights_keyword( }); } - async fn set_reflection_enabled( state: &AppState, bot_id: Uuid, @@ -1077,5 +1011,3 @@ async fn set_reflection_enabled( if enabled { "enabled" } else { "disabled" } )) } - - diff --git a/src/basic/keywords/code_sandbox.rs b/src/basic/keywords/code_sandbox.rs index 5d8708fa2..a60a2cd43 100644 --- a/src/basic/keywords/code_sandbox.rs +++ b/src/basic/keywords/code_sandbox.rs @@ -1,25 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - use crate::shared::models::UserSession; use crate::shared::state::AppState; use diesel::prelude::*; @@ -33,10 +11,8 @@ use std::time::Duration; use tokio::time::timeout; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum SandboxRuntime { - LXC, Docker, @@ -63,7 +39,6 @@ impl From<&str> for SandboxRuntime { } } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum CodeLanguage { Python, @@ -108,10 +83,8 @@ impl CodeLanguage { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxConfig { - pub enabled: bool, pub runtime: SandboxRuntime, @@ -151,7 +124,6 @@ impl Default for SandboxConfig { } impl SandboxConfig { - pub fn from_bot_config(state: &AppState, bot_id: Uuid) -> Self { let mut config = Self::default(); @@ -218,10 +190,8 @@ impl SandboxConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionResult { - pub stdout: String, pub stderr: String, @@ -289,7 +259,6 @@ impl ExecutionResult { } } - pub struct CodeSandbox { config: SandboxConfig, session_id: Uuid, @@ -309,7 +278,6 @@ impl CodeSandbox { Self { config, session_id } } - pub async fn execute(&self, code: &str, language: CodeLanguage) -> ExecutionResult { if !self.config.enabled { return ExecutionResult::error("Sandbox execution is disabled"); @@ -339,20 +307,16 @@ impl CodeSandbox { } } - async fn execute_lxc( &self, code: &str, language: &CodeLanguage, ) -> Result { - let container_name = format!("gb-sandbox-{}", Uuid::new_v4()); - std::fs::create_dir_all(&self.config.work_dir) .map_err(|e| format!("Failed to create work dir: {}", e))?; - let code_file = format!( "{}/{}.{}", self.config.work_dir, @@ -362,7 +326,6 @@ impl CodeSandbox { std::fs::write(&code_file, code) .map_err(|e| format!("Failed to write code file: {}", e))?; - let mut cmd = Command::new("lxc-execute"); cmd.arg("-n") .arg(&container_name) @@ -372,7 +335,6 @@ impl CodeSandbox { .arg(language.interpreter()) .arg(&code_file); - cmd.env( "LXC_CGROUP_MEMORY_LIMIT", format!("{}M", self.config.memory_limit_mb), @@ -382,7 +344,6 @@ impl CodeSandbox { format!("{}", self.config.cpu_limit_percent * 1000), ); - let timeout_duration = Duration::from_secs(self.config.timeout_seconds); let output = timeout(timeout_duration, async { tokio::process::Command::new("lxc-execute") @@ -398,7 +359,6 @@ impl CodeSandbox { }) .await; - let _ = std::fs::remove_file(&code_file); match output { @@ -422,20 +382,17 @@ impl CodeSandbox { } } - async fn execute_docker( &self, code: &str, language: &CodeLanguage, ) -> Result { - let image = match language { CodeLanguage::Python => "python:3.11-slim", CodeLanguage::JavaScript => "node:20-slim", CodeLanguage::Bash => "alpine:latest", }; - let args = vec![ "run".to_string(), "--rm".to_string(), @@ -459,7 +416,6 @@ impl CodeSandbox { code.to_string(), ]; - let timeout_duration = Duration::from_secs(self.config.timeout_seconds); let output = timeout(timeout_duration, async { tokio::process::Command::new("docker") @@ -490,42 +446,34 @@ impl CodeSandbox { } } - async fn execute_firecracker( &self, code: &str, language: &CodeLanguage, ) -> Result { - - warn!("Firecracker runtime not yet implemented, falling back to process isolation"); self.execute_process(code, language).await } - async fn execute_process( &self, code: &str, language: &CodeLanguage, ) -> Result { - let temp_dir = format!("{}/{}", self.config.work_dir, Uuid::new_v4()); std::fs::create_dir_all(&temp_dir) .map_err(|e| format!("Failed to create temp dir: {}", e))?; - let code_file = format!("{}/code.{}", temp_dir, language.file_extension()); std::fs::write(&code_file, code) .map_err(|e| format!("Failed to write code file: {}", e))?; - let (cmd_name, cmd_args): (&str, Vec<&str>) = match language { CodeLanguage::Python => ("python3", vec![&code_file]), CodeLanguage::JavaScript => ("node", vec![&code_file]), CodeLanguage::Bash => ("bash", vec![&code_file]), }; - let timeout_duration = Duration::from_secs(self.config.timeout_seconds); let output = timeout(timeout_duration, async { tokio::process::Command::new(cmd_name) @@ -540,7 +488,6 @@ impl CodeSandbox { }) .await; - let _ = std::fs::remove_dir_all(&temp_dir); match output { @@ -564,7 +511,6 @@ impl CodeSandbox { } } - pub async fn execute_file(&self, file_path: &str, language: CodeLanguage) -> ExecutionResult { match std::fs::read_to_string(file_path) { Ok(code) => self.execute(&code, language).await, @@ -573,7 +519,6 @@ impl CodeSandbox { } } - pub fn register_sandbox_keywords(state: Arc, user: UserSession, engine: &mut Engine) { run_python_keyword(state.clone(), user.clone(), engine); run_javascript_keyword(state.clone(), user.clone(), engine); @@ -581,7 +526,6 @@ pub fn register_sandbox_keywords(state: Arc, user: UserSession, engine run_file_keyword(state.clone(), user.clone(), engine); } - pub fn run_python_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -627,7 +571,6 @@ pub fn run_python_keyword(state: Arc, user: UserSession, engine: &mut .expect("Failed to register RUN PYTHON syntax"); } - pub fn run_javascript_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -672,7 +615,6 @@ pub fn run_javascript_keyword(state: Arc, user: UserSession, engine: & ) .expect("Failed to register RUN JAVASCRIPT syntax"); - let state_clone2 = Arc::clone(&state); let user_clone2 = user.clone(); @@ -711,7 +653,6 @@ pub fn run_javascript_keyword(state: Arc, user: UserSession, engine: & .expect("Failed to register RUN JS syntax"); } - pub fn run_bash_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -753,7 +694,6 @@ pub fn run_bash_keyword(state: Arc, user: UserSession, engine: &mut En .expect("Failed to register RUN BASH syntax"); } - pub fn run_file_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -802,7 +742,6 @@ pub fn run_file_keyword(state: Arc, user: UserSession, engine: &mut En ) .expect("Failed to register RUN PYTHON WITH FILE syntax"); - let state_clone2 = Arc::clone(&state); let user_clone2 = user.clone(); @@ -847,11 +786,8 @@ pub fn run_file_keyword(state: Arc, user: UserSession, engine: &mut En .expect("Failed to register RUN JAVASCRIPT WITH FILE syntax"); } - - - pub fn generate_python_lxc_config() -> String { - r#" + r" # LXC configuration for Python sandbox lxc.include = /usr/share/lxc/config/common.conf lxc.arch = linux64 @@ -879,13 +815,12 @@ lxc.mount.auto = proc:mixed sys:ro lxc.mount.entry = /usr/bin/python3 usr/bin/python3 none ro,bind 0 0 lxc.mount.entry = /usr/lib/python3 usr/lib/python3 none ro,bind 0 0 lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0 -"# +" .to_string() } - pub fn generate_node_lxc_config() -> String { - r#" + r" # LXC configuration for Node.js sandbox lxc.include = /usr/share/lxc/config/common.conf lxc.arch = linux64 @@ -913,8 +848,6 @@ lxc.mount.auto = proc:mixed sys:ro lxc.mount.entry = /usr/bin/node usr/bin/node none ro,bind 0 0 lxc.mount.entry = /usr/lib/node_modules usr/lib/node_modules none ro,bind 0 0 lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0 -"# +" .to_string() } - - diff --git a/src/basic/keywords/create_site.rs b/src/basic/keywords/create_site.rs index 1623a615e..42c897e15 100644 --- a/src/basic/keywords/create_site.rs +++ b/src/basic/keywords/create_site.rs @@ -1,8 +1,3 @@ - - - - - use crate::llm::LLMProvider; use crate::shared::models::UserSession; use crate::shared::state::AppState; @@ -16,7 +11,6 @@ use std::io::Read; use std::path::PathBuf; use std::sync::Arc; - pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Engine) { let state_clone = state.clone(); let user_clone = user.clone(); @@ -58,12 +52,6 @@ pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Eng .unwrap(); } - - - - - - async fn create_site( config: crate::config::AppConfig, s3: Option>, @@ -83,20 +71,16 @@ async fn create_site( alias_str, template_dir_str ); - let base_path = PathBuf::from(&config.site_path); let template_path = base_path.join(&template_dir_str); let combined_content = load_templates(&template_path)?; - let generated_html = generate_html_from_prompt(llm, &combined_content, &prompt_str).await?; - let drive_path = format!("apps/{}", alias_str); store_to_drive(&s3, &bucket, &bot_id, &drive_path, &generated_html).await?; - let serve_path = base_path.join(&alias_str); sync_to_serve_path(&serve_path, &generated_html, &template_path).await?; @@ -108,7 +92,6 @@ async fn create_site( Ok(format!("/apps/{}", alias_str)) } - fn load_templates(template_path: &PathBuf) -> Result> { let mut combined_content = String::new(); @@ -141,7 +124,6 @@ fn load_templates(template_path: &PathBuf) -> Result>, templates: &str, @@ -209,7 +191,6 @@ OUTPUT: Complete index.html file only, no explanations."#, Ok(html) } - fn extract_html_from_response(response: &str) -> String { let trimmed = response.trim(); @@ -234,7 +215,6 @@ fn extract_html_from_response(response: &str) -> String { trimmed.to_string() } - fn generate_placeholder_html(prompt: &str) -> String { format!( r##" @@ -278,7 +258,6 @@ fn generate_placeholder_html(prompt: &str) -> String { ) } - async fn store_to_drive( s3: &Option>, bucket: &str, @@ -304,7 +283,6 @@ async fn store_to_drive( .await .map_err(|e| format!("Failed to store to drive: {}", e))?; - let schema_key = format!("{}.gbdrive/{}/schema.json", bot_id, drive_path); let schema = r#"{"tables": {}, "version": 1}"#; @@ -321,23 +299,19 @@ async fn store_to_drive( Ok(()) } - async fn sync_to_serve_path( serve_path: &PathBuf, html_content: &str, template_path: &PathBuf, ) -> Result<(), Box> { - fs::create_dir_all(serve_path).map_err(|e| format!("Failed to create serve path: {}", e))?; - let index_path = serve_path.join("index.html"); fs::write(&index_path, html_content) .map_err(|e| format!("Failed to write index.html: {}", e))?; info!("Written: {:?}", index_path); - let template_assets = template_path.join("_assets"); let serve_assets = serve_path.join("_assets"); @@ -345,26 +319,21 @@ async fn sync_to_serve_path( copy_dir_recursive(&template_assets, &serve_assets)?; info!("Copied assets to: {:?}", serve_assets); } else { - fs::create_dir_all(&serve_assets) .map_err(|e| format!("Failed to create assets dir: {}", e))?; - let htmx_path = serve_assets.join("htmx.min.js"); if !htmx_path.exists() { - fs::write(&htmx_path, "/* HTMX - include from CDN or bundle */") .map_err(|e| format!("Failed to write htmx: {}", e))?; } - let styles_path = serve_assets.join("styles.css"); if !styles_path.exists() { fs::write(&styles_path, DEFAULT_STYLES) .map_err(|e| format!("Failed to write styles: {}", e))?; } - let app_js_path = serve_assets.join("app.js"); if !app_js_path.exists() { fs::write(&app_js_path, DEFAULT_APP_JS) @@ -372,7 +341,6 @@ async fn sync_to_serve_path( } } - let schema_path = serve_path.join("schema.json"); fs::write(&schema_path, r#"{"tables": {}, "version": 1}"#) .map_err(|e| format!("Failed to write schema.json: {}", e))?; @@ -380,7 +348,6 @@ async fn sync_to_serve_path( Ok(()) } - fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), Box> { fs::create_dir_all(dst).map_err(|e| format!("Failed to create dir {:?}: {}", dst, e))?; @@ -400,8 +367,7 @@ fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), Box, @@ -94,7 +51,6 @@ pub struct ActionItem { pub completed: bool, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Priority { @@ -110,10 +66,8 @@ impl Default for Priority { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Sentiment { - pub score: f64, pub label: SentimentLabel, @@ -121,7 +75,6 @@ pub struct Sentiment { pub confidence: f64, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum SentimentLabel { @@ -148,7 +101,6 @@ impl Default for Sentiment { } } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ResolutionStatus { @@ -165,10 +117,8 @@ impl Default for ResolutionStatus { } } - #[derive(Debug, Clone)] pub struct EpisodicMemoryConfig { - pub enabled: bool, pub threshold: usize, @@ -198,7 +148,6 @@ impl Default for EpisodicMemoryConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConversationMessage { pub id: Uuid, @@ -207,19 +156,16 @@ pub struct ConversationMessage { pub timestamp: DateTime, } - #[derive(Debug)] pub struct EpisodicMemoryManager { config: EpisodicMemoryConfig, } impl EpisodicMemoryManager { - pub fn new(config: EpisodicMemoryConfig) -> Self { EpisodicMemoryManager { config } } - pub fn from_config(config_map: &std::collections::HashMap) -> Self { let config = EpisodicMemoryConfig { enabled: config_map @@ -254,22 +200,18 @@ impl EpisodicMemoryManager { EpisodicMemoryManager::new(config) } - pub fn should_summarize(&self, message_count: usize) -> bool { self.config.enabled && self.config.auto_summarize && message_count >= self.config.threshold } - pub fn get_history_to_keep(&self) -> usize { self.config.history } - pub fn get_threshold(&self) -> usize { self.config.threshold } - pub fn generate_summary_prompt(&self, messages: &[ConversationMessage]) -> String { let formatted_messages = messages .iter() @@ -309,7 +251,6 @@ Respond with valid JSON only: ) } - pub fn parse_summary_response( &self, response: &str, @@ -318,7 +259,6 @@ Respond with valid JSON only: bot_id: Uuid, session_id: Uuid, ) -> Result { - let json_str = extract_json(response)?; let parsed: serde_json::Value = @@ -418,22 +358,18 @@ Respond with valid JSON only: }) } - pub fn get_retention_cutoff(&self) -> DateTime { Utc::now() - Duration::days(self.config.retention_days as i64) } } - -fn extract_json(response: &str) -> Result { - +pub fn extract_json(response: &str) -> Result { if let Some(start) = response.find("```json") { if let Some(end) = response[start + 7..].find("```") { return Ok(response[start + 7..start + 7 + end].trim().to_string()); } } - if let Some(start) = response.find("```") { let after_start = start + 3; @@ -446,7 +382,6 @@ fn extract_json(response: &str) -> Result { } } - if let Some(start) = response.find('{') { if let Some(end) = response.rfind('}') { if end > start { @@ -458,7 +393,6 @@ fn extract_json(response: &str) -> Result { Err("No JSON found in response".to_string()) } - impl Episode { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -531,12 +465,7 @@ impl Episode { } } - pub fn register_episodic_memory_keywords(engine: &mut Engine) { - - - - engine.register_fn("episode_summary", |episode: Map| -> String { episode .get("summary") @@ -584,7 +513,6 @@ pub fn register_episodic_memory_keywords(engine: &mut Engine) { info!("Episodic memory keywords registered"); } - pub const EPISODIC_MEMORY_SCHEMA: &str = r#" -- Conversation episodes (summaries) CREATE TABLE IF NOT EXISTS conversation_episodes ( @@ -619,9 +547,8 @@ CREATE INDEX IF NOT EXISTS idx_episodes_summary_fts ON conversation_episodes USING GIN(to_tsvector('english', summary)); "#; - pub mod sql { - pub const INSERT_EPISODE: &str = r#" + pub const INSERT_EPISODE: &str = r" INSERT INTO conversation_episodes ( id, user_id, bot_id, session_id, summary, key_topics, decisions, action_items, sentiment, resolution, message_count, message_ids, @@ -629,22 +556,22 @@ pub mod sql { ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 ) - "#; + "; - pub const GET_EPISODES_BY_USER: &str = r#" + pub const GET_EPISODES_BY_USER: &str = r" SELECT * FROM conversation_episodes WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 - "#; + "; - pub const GET_EPISODES_BY_SESSION: &str = r#" + pub const GET_EPISODES_BY_SESSION: &str = r" SELECT * FROM conversation_episodes WHERE session_id = $1 ORDER BY created_at DESC - "#; + "; - pub const SEARCH_EPISODES: &str = r#" + pub const SEARCH_EPISODES: &str = r" SELECT * FROM conversation_episodes WHERE user_id = $1 AND ( @@ -653,19 +580,19 @@ pub mod sql { ) ORDER BY created_at DESC LIMIT $4 - "#; + "; - pub const DELETE_OLD_EPISODES: &str = r#" + pub const DELETE_OLD_EPISODES: &str = r" DELETE FROM conversation_episodes WHERE created_at < $1 - "#; + "; - pub const COUNT_USER_EPISODES: &str = r#" + pub const COUNT_USER_EPISODES: &str = r" SELECT COUNT(*) FROM conversation_episodes WHERE user_id = $1 - "#; + "; - pub const DELETE_OLDEST_EPISODES: &str = r#" + pub const DELETE_OLDEST_EPISODES: &str = r" DELETE FROM conversation_episodes WHERE id IN ( SELECT id FROM conversation_episodes @@ -673,5 +600,5 @@ pub mod sql { ORDER BY created_at ASC LIMIT $2 ) - "#; + "; } diff --git a/src/basic/keywords/first.rs b/src/basic/keywords/first.rs index a946480af..4efc3f697 100644 --- a/src/basic/keywords/first.rs +++ b/src/basic/keywords/first.rs @@ -3,7 +3,7 @@ use rhai::Engine; pub fn first_keyword(engine: &mut Engine) { engine - .register_custom_syntax(&["FIRST", "$expr$"], false, { + .register_custom_syntax(["FIRST", "$expr$"], false, { move |context, inputs| { let input_string = context.eval_expression_tree(&inputs[0])?; let input_str = input_string.to_string(); diff --git a/src/basic/keywords/hear_talk.rs b/src/basic/keywords/hear_talk.rs index 472a8811b..80665a5ed 100644 --- a/src/basic/keywords/hear_talk.rs +++ b/src/basic/keywords/hear_talk.rs @@ -1,29 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - use crate::shared::message_types::MessageType; use crate::shared::models::{BotResponse, UserSession}; use crate::shared::state::AppState; @@ -34,8 +8,7 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum InputType { Any, Email, @@ -67,81 +40,74 @@ pub enum InputType { } impl InputType { - + #[must_use] pub fn error_message(&self) -> String { match self { - InputType::Any => "".to_string(), - InputType::Email => { + Self::Any => String::new(), + Self::Email => { "Please enter a valid email address (e.g., user@example.com)".to_string() } - InputType::Date => { - "Please enter a valid date (e.g., 25/12/2024 or 2024-12-25)".to_string() - } - InputType::Name => "Please enter a valid name (letters and spaces only)".to_string(), - InputType::Integer => "Please enter a valid whole number".to_string(), - InputType::Float => "Please enter a valid number".to_string(), - InputType::Boolean => "Please answer yes or no".to_string(), - InputType::Hour => "Please enter a valid time (e.g., 14:30 or 2:30 PM)".to_string(), - InputType::Money => { - "Please enter a valid amount (e.g., 100.00 or R$ 100,00)".to_string() - } - InputType::Mobile => "Please enter a valid mobile number".to_string(), - InputType::Zipcode => "Please enter a valid ZIP/postal code".to_string(), - InputType::Language => { - "Please enter a valid language code (e.g., en, pt, es)".to_string() - } - InputType::Cpf => "Please enter a valid CPF (11 digits)".to_string(), - InputType::Cnpj => "Please enter a valid CNPJ (14 digits)".to_string(), - InputType::QrCode => "Please send an image containing a QR code".to_string(), - InputType::Login => "Please complete the authentication process".to_string(), - InputType::Menu(options) => format!("Please select one of: {}", options.join(", ")), - InputType::File => "Please upload a file".to_string(), - InputType::Image => "Please send an image".to_string(), - InputType::Audio => "Please send an audio file or voice message".to_string(), - InputType::Video => "Please send a video".to_string(), - InputType::Document => "Please send a document (PDF, Word, etc.)".to_string(), - InputType::Url => "Please enter a valid URL".to_string(), - InputType::Uuid => "Please enter a valid UUID".to_string(), - InputType::Color => "Please enter a valid color (e.g., #FF0000 or red)".to_string(), - InputType::CreditCard => "Please enter a valid credit card number".to_string(), - InputType::Password => "Please enter a password (minimum 8 characters)".to_string(), + Self::Date => "Please enter a valid date (e.g., 25/12/2024 or 2024-12-25)".to_string(), + Self::Name => "Please enter a valid name (letters and spaces only)".to_string(), + Self::Integer => "Please enter a valid whole number".to_string(), + Self::Float => "Please enter a valid number".to_string(), + Self::Boolean => "Please answer yes or no".to_string(), + Self::Hour => "Please enter a valid time (e.g., 14:30 or 2:30 PM)".to_string(), + Self::Money => "Please enter a valid amount (e.g., 100.00 or R$ 100,00)".to_string(), + Self::Mobile => "Please enter a valid mobile number".to_string(), + Self::Zipcode => "Please enter a valid ZIP/postal code".to_string(), + Self::Language => "Please enter a valid language code (e.g., en, pt, es)".to_string(), + Self::Cpf => "Please enter a valid CPF (11 digits)".to_string(), + Self::Cnpj => "Please enter a valid CNPJ (14 digits)".to_string(), + Self::QrCode => "Please send an image containing a QR code".to_string(), + Self::Login => "Please complete the authentication process".to_string(), + Self::Menu(options) => format!("Please select one of: {}", options.join(", ")), + Self::File => "Please upload a file".to_string(), + Self::Image => "Please send an image".to_string(), + Self::Audio => "Please send an audio file or voice message".to_string(), + Self::Video => "Please send a video".to_string(), + Self::Document => "Please send a document (PDF, Word, etc.)".to_string(), + Self::Url => "Please enter a valid URL".to_string(), + Self::Uuid => "Please enter a valid UUID".to_string(), + Self::Color => "Please enter a valid color (e.g., #FF0000 or red)".to_string(), + Self::CreditCard => "Please enter a valid credit card number".to_string(), + Self::Password => "Please enter a password (minimum 8 characters)".to_string(), } } - + #[must_use] pub fn from_str(s: &str) -> Self { match s.to_uppercase().as_str() { - "EMAIL" => InputType::Email, - "DATE" => InputType::Date, - "NAME" => InputType::Name, - "INTEGER" | "INT" | "NUMBER" => InputType::Integer, - "FLOAT" | "DECIMAL" | "DOUBLE" => InputType::Float, - "BOOLEAN" | "BOOL" => InputType::Boolean, - "HOUR" | "TIME" => InputType::Hour, - "MONEY" | "CURRENCY" | "AMOUNT" => InputType::Money, - "MOBILE" | "PHONE" | "TELEPHONE" => InputType::Mobile, - "ZIPCODE" | "ZIP" | "CEP" | "POSTALCODE" => InputType::Zipcode, - "LANGUAGE" | "LANG" => InputType::Language, - "CPF" => InputType::Cpf, - "CNPJ" => InputType::Cnpj, - "QRCODE" | "QR" => InputType::QrCode, - "LOGIN" | "AUTH" => InputType::Login, - "FILE" => InputType::File, - "IMAGE" | "PHOTO" | "PICTURE" => InputType::Image, - "AUDIO" | "VOICE" | "SOUND" => InputType::Audio, - "VIDEO" => InputType::Video, - "DOCUMENT" | "DOC" | "PDF" => InputType::Document, - "URL" | "LINK" => InputType::Url, - "UUID" | "GUID" => InputType::Uuid, - "COLOR" | "COLOUR" => InputType::Color, - "CREDITCARD" | "CARD" => InputType::CreditCard, - "PASSWORD" | "PASS" | "SECRET" => InputType::Password, - _ => InputType::Any, + "EMAIL" => Self::Email, + "DATE" => Self::Date, + "NAME" => Self::Name, + "INTEGER" | "INT" | "NUMBER" => Self::Integer, + "FLOAT" | "DECIMAL" | "DOUBLE" => Self::Float, + "BOOLEAN" | "BOOL" => Self::Boolean, + "HOUR" | "TIME" => Self::Hour, + "MONEY" | "CURRENCY" | "AMOUNT" => Self::Money, + "MOBILE" | "PHONE" | "TELEPHONE" => Self::Mobile, + "ZIPCODE" | "ZIP" | "CEP" | "POSTALCODE" => Self::Zipcode, + "LANGUAGE" | "LANG" => Self::Language, + "CPF" => Self::Cpf, + "CNPJ" => Self::Cnpj, + "QRCODE" | "QR" => Self::QrCode, + "LOGIN" | "AUTH" => Self::Login, + "FILE" => Self::File, + "IMAGE" | "PHOTO" | "PICTURE" => Self::Image, + "AUDIO" | "VOICE" | "SOUND" => Self::Audio, + "VIDEO" => Self::Video, + "DOCUMENT" | "DOC" | "PDF" => Self::Document, + "URL" | "LINK" => Self::Url, + "UUID" | "GUID" => Self::Uuid, + "COLOR" | "COLOUR" => Self::Color, + "CREDITCARD" | "CARD" => Self::CreditCard, + "PASSWORD" | "PASS" | "SECRET" => Self::Password, + _ => Self::Any, } } } - #[derive(Debug, Clone)] pub struct ValidationResult { pub is_valid: bool, @@ -151,6 +117,7 @@ pub struct ValidationResult { } impl ValidationResult { + #[must_use] pub fn valid(value: String) -> Self { Self { is_valid: true, @@ -160,6 +127,7 @@ impl ValidationResult { } } + #[must_use] pub fn valid_with_metadata(value: String, metadata: serde_json::Value) -> Self { Self { is_valid: true, @@ -169,6 +137,7 @@ impl ValidationResult { } } + #[must_use] pub fn invalid(error: String) -> Self { Self { is_valid: false, @@ -179,26 +148,20 @@ impl ValidationResult { } } - pub fn hear_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - register_hear_basic(state.clone(), user.clone(), engine); - register_hear_as_type(state.clone(), user.clone(), engine); - register_hear_as_menu(state.clone(), user.clone(), engine); } - fn register_hear_basic(state: Arc, user: UserSession, engine: &mut Engine) { let session_id = user.id; let state_clone = Arc::clone(&state); engine .register_custom_syntax(&["HEAR", "$ident$"], true, move |_context, inputs| { - let variable_name = inputs[0] .get_string_value() .expect("Expected identifier as string") @@ -223,10 +186,9 @@ fn register_hear_basic(state: Arc, user: UserSession, engine: &mut Eng let mut session_manager = state_for_spawn.session_manager.lock().await; session_manager.mark_waiting(session_id_clone); - if let Some(redis_client) = &state_for_spawn.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - let key = format!("hear:{}:{}", session_id_clone, var_name_clone); + let key = format!("hear:{session_id_clone}:{var_name_clone}"); let wait_data = serde_json::json!({ "variable": var_name_clone, "type": "any", @@ -252,7 +214,6 @@ fn register_hear_basic(state: Arc, user: UserSession, engine: &mut Eng .unwrap(); } - fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut Engine) { let session_id = user.id; let state_clone = Arc::clone(&state); @@ -262,7 +223,6 @@ fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut E &["HEAR", "$ident$", "AS", "$ident$"], true, move |_context, inputs| { - let variable_name = inputs[0] .get_string_value() .expect("Expected identifier for variable") @@ -274,11 +234,7 @@ fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut E let _input_type = InputType::from_str(&type_name); - trace!( - "HEAR {} AS {} - waiting for validated input", - variable_name, - type_name - ); + trace!("HEAR {variable_name} AS {type_name} - waiting for validated input"); let state_for_spawn = Arc::clone(&state_clone); let session_id_clone = session_id; @@ -289,11 +245,10 @@ fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut E let mut session_manager = state_for_spawn.session_manager.lock().await; session_manager.mark_waiting(session_id_clone); - if let Some(redis_client) = &state_for_spawn.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - let key = format!("hear:{}:{}", session_id_clone, var_name_clone); + let key = format!("hear:{session_id_clone}:{var_name_clone}"); let wait_data = serde_json::json!({ "variable": var_name_clone, "type": type_clone.to_lowercase(), @@ -321,44 +276,34 @@ fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut E .unwrap(); } - fn register_hear_as_menu(state: Arc, user: UserSession, engine: &mut Engine) { let session_id = user.id; let state_clone = Arc::clone(&state); - - engine .register_custom_syntax( &["HEAR", "$ident$", "AS", "$expr$"], true, move |context, inputs| { - let variable_name = inputs[0] .get_string_value() .expect("Expected identifier for variable") .to_lowercase(); - let options_expr = context.eval_expression_tree(&inputs[1])?; let options_str = options_expr.to_string(); - let input_type = InputType::from_str(&options_str); if input_type != InputType::Any { - return Err(Box::new(EvalAltResult::ErrorRuntime( "Use HEAR AS TYPE syntax".into(), rhai::Position::NONE, ))); } - let options: Vec = if options_str.starts_with('[') { - serde_json::from_str(&options_str).unwrap_or_default() } else { - options_str .split(',') .map(|s| s.trim().trim_matches('"').to_string()) @@ -384,11 +329,10 @@ fn register_hear_as_menu(state: Arc, user: UserSession, engine: &mut E let mut session_manager = state_for_spawn.session_manager.lock().await; session_manager.mark_waiting(session_id_clone); - if let Some(redis_client) = &state_for_spawn.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - let key = format!("hear:{}:{}", session_id_clone, var_name_clone); + let key = format!("hear:{session_id_clone}:{var_name_clone}"); let wait_data = serde_json::json!({ "variable": var_name_clone, "type": "menu", @@ -404,9 +348,8 @@ fn register_hear_as_menu(state: Arc, user: UserSession, engine: &mut E .query_async(&mut conn) .await; - let suggestions_key = - format!("suggestions:{}:{}", session_id_clone, session_id_clone); + format!("suggestions:{session_id_clone}:{session_id_clone}"); for opt in &options_clone { let suggestion = serde_json::json!({ "text": opt, @@ -431,15 +374,19 @@ fn register_hear_as_menu(state: Arc, user: UserSession, engine: &mut E .unwrap(); } - - - +#[must_use] pub fn validate_input(input: &str, input_type: &InputType) -> ValidationResult { let trimmed = input.trim(); match input_type { - InputType::Any => ValidationResult::valid(trimmed.to_string()), - + InputType::Any + | InputType::QrCode + | InputType::File + | InputType::Image + | InputType::Audio + | InputType::Video + | InputType::Document + | InputType::Login => ValidationResult::valid(trimmed.to_string()), InputType::Email => validate_email(trimmed), InputType::Date => validate_date(trimmed), InputType::Name => validate_name(trimmed), @@ -458,18 +405,7 @@ pub fn validate_input(input: &str, input_type: &InputType) -> ValidationResult { InputType::Color => validate_color(trimmed), InputType::CreditCard => validate_credit_card(trimmed), InputType::Password => validate_password(trimmed), - InputType::Menu(options) => validate_menu(trimmed, options), - - - InputType::QrCode - | InputType::File - | InputType::Image - | InputType::Audio - | InputType::Video - | InputType::Document => ValidationResult::valid(trimmed.to_string()), - - InputType::Login => ValidationResult::valid(trimmed.to_string()), } } @@ -486,32 +422,23 @@ fn validate_email(input: &str) -> ValidationResult { } fn validate_date(input: &str) -> ValidationResult { - let formats = [ - "%d/%m/%Y", - "%d-%m-%Y", - "%Y-%m-%d", - "%Y/%m/%d", - "%d.%m.%Y", - "%m/%d/%Y", - "%d %b %Y", + "%d/%m/%Y", "%d-%m-%Y", "%Y-%m-%d", "%Y/%m/%d", "%d.%m.%Y", "%m/%d/%Y", "%d %b %Y", "%d %B %Y", ]; for format in &formats { if let Ok(date) = chrono::NaiveDate::parse_from_str(input, format) { - return ValidationResult::valid_with_metadata( date.format("%Y-%m-%d").to_string(), serde_json::json!({ "original": input, - "parsed_format": format + "parsed_format": *format }), ); } } - let lower = input.to_lowercase(); let today = chrono::Local::now().date_naive(); @@ -537,7 +464,6 @@ fn validate_date(input: &str) -> ValidationResult { } fn validate_name(input: &str) -> ValidationResult { - let name_regex = Regex::new(r"^[\p{L}\s\-']+$").unwrap(); if input.len() < 2 { @@ -549,7 +475,6 @@ fn validate_name(input: &str) -> ValidationResult { } if name_regex.is_match(input) { - let normalized = input .split_whitespace() .map(|word| { @@ -568,11 +493,10 @@ fn validate_name(input: &str) -> ValidationResult { } fn validate_integer(input: &str) -> ValidationResult { - let cleaned = input - .replace(",", "") - .replace(".", "") - .replace(" ", "") + .replace(',', "") + .replace('.', "") + .replace(' ', "") .trim() .to_string(); @@ -586,7 +510,6 @@ fn validate_integer(input: &str) -> ValidationResult { } fn validate_float(input: &str) -> ValidationResult { - let cleaned = input.replace(" ", "").replace(",", ".").trim().to_string(); match cleaned.parse::() { @@ -644,7 +567,6 @@ fn validate_boolean(input: &str) -> ValidationResult { } fn validate_hour(input: &str) -> ValidationResult { - let time_24_regex = Regex::new(r"^([01]?\d|2[0-3]):([0-5]\d)$").unwrap(); if let Some(caps) = time_24_regex.captures(input) { let hour: u32 = caps[1].parse().unwrap(); @@ -655,7 +577,6 @@ fn validate_hour(input: &str) -> ValidationResult { ); } - let time_12_regex = Regex::new(r"^(1[0-2]|0?[1-9]):([0-5]\d)\s*(AM|PM|am|pm|a\.m\.|p\.m\.)$").unwrap(); if let Some(caps) = time_12_regex.captures(input) { @@ -679,7 +600,6 @@ fn validate_hour(input: &str) -> ValidationResult { } fn validate_money(input: &str) -> ValidationResult { - let cleaned = input .replace("R$", "") .replace("$", "") @@ -690,21 +610,16 @@ fn validate_money(input: &str) -> ValidationResult { .trim() .to_string(); - let normalized = if cleaned.contains(',') && cleaned.contains('.') { - let last_comma = cleaned.rfind(',').unwrap_or(0); let last_dot = cleaned.rfind('.').unwrap_or(0); if last_comma > last_dot { - - cleaned.replace(".", "").replace(",", ".") + cleaned.replace('.', "").replace(',', ".") } else { - cleaned.replace(",", "") } } else if cleaned.contains(',') { - cleaned.replace(",", ".") } else { cleaned @@ -720,26 +635,19 @@ fn validate_money(input: &str) -> ValidationResult { } fn validate_mobile(input: &str) -> ValidationResult { - let digits: String = input.chars().filter(|c| c.is_ascii_digit()).collect(); - if digits.len() < 10 || digits.len() > 15 { return ValidationResult::invalid(InputType::Mobile.error_message()); } - let formatted = if digits.len() == 11 && digits.starts_with('9') { - format!("({}) {}-{}", &digits[0..2], &digits[2..7], &digits[7..11]) } else if digits.len() == 11 { - format!("({}) {}-{}", &digits[0..2], &digits[2..7], &digits[7..11]) } else if digits.len() == 10 { - format!("({}) {}-{}", &digits[0..3], &digits[3..6], &digits[6..10]) } else { - format!("+{}", digits) }; @@ -750,13 +658,11 @@ fn validate_mobile(input: &str) -> ValidationResult { } fn validate_zipcode(input: &str) -> ValidationResult { - let cleaned: String = input .chars() .filter(|c| c.is_ascii_alphanumeric()) .collect(); - if cleaned.len() == 8 && cleaned.chars().all(|c| c.is_ascii_digit()) { let formatted = format!("{}-{}", &cleaned[0..5], &cleaned[5..8]); return ValidationResult::valid_with_metadata( @@ -765,10 +671,9 @@ fn validate_zipcode(input: &str) -> ValidationResult { ); } - if (cleaned.len() == 5 || cleaned.len() == 9) && cleaned.chars().all(|c| c.is_ascii_digit()) { let formatted = if cleaned.len() == 9 { - format!("{}-{}", &cleaned[0..5], &cleaned[5..9]) + format!("{}-{}", &cleaned[0..5], &cleaned[5..8]) } else { cleaned.clone() }; @@ -778,7 +683,6 @@ fn validate_zipcode(input: &str) -> ValidationResult { ); } - let uk_regex = Regex::new(r"^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$").unwrap(); if uk_regex.is_match(&cleaned.to_uppercase()) { return ValidationResult::valid_with_metadata( @@ -793,7 +697,6 @@ fn validate_zipcode(input: &str) -> ValidationResult { fn validate_language(input: &str) -> ValidationResult { let lower = input.to_lowercase().trim().to_string(); - let languages = [ ("en", "english", "inglês", "ingles"), ("pt", "portuguese", "português", "portugues"), @@ -827,7 +730,6 @@ fn validate_language(input: &str) -> ValidationResult { } } - if lower.len() == 2 && lower.chars().all(|c| c.is_ascii_lowercase()) { return ValidationResult::valid(lower); } @@ -836,21 +738,17 @@ fn validate_language(input: &str) -> ValidationResult { } fn validate_cpf(input: &str) -> ValidationResult { - let digits: String = input.chars().filter(|c| c.is_ascii_digit()).collect(); if digits.len() != 11 { return ValidationResult::invalid(InputType::Cpf.error_message()); } - if digits.chars().all(|c| c == digits.chars().next().unwrap()) { return ValidationResult::invalid("Invalid CPF".to_string()); } - - let digits_vec: Vec = digits.chars().map(|c| c.to_digit(10).unwrap()).collect(); - + let digits_vec: Vec = digits.chars().filter_map(|c| c.to_digit(10)).collect(); let sum1: u32 = digits_vec[0..9] .iter() @@ -864,7 +762,6 @@ fn validate_cpf(input: &str) -> ValidationResult { return ValidationResult::invalid("Invalid CPF".to_string()); } - let sum2: u32 = digits_vec[0..10] .iter() .enumerate() @@ -877,7 +774,6 @@ fn validate_cpf(input: &str) -> ValidationResult { return ValidationResult::invalid("Invalid CPF".to_string()); } - let formatted = format!( "{}.{}.{}-{}", &digits[0..3], @@ -893,16 +789,13 @@ fn validate_cpf(input: &str) -> ValidationResult { } fn validate_cnpj(input: &str) -> ValidationResult { - let digits: String = input.chars().filter(|c| c.is_ascii_digit()).collect(); if digits.len() != 14 { return ValidationResult::invalid(InputType::Cnpj.error_message()); } - - let digits_vec: Vec = digits.chars().map(|c| c.to_digit(10).unwrap()).collect(); - + let digits_vec: Vec = digits.chars().filter_map(|c| c.to_digit(10)).collect(); let weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let sum1: u32 = digits_vec[0..12] @@ -917,7 +810,6 @@ fn validate_cnpj(input: &str) -> ValidationResult { return ValidationResult::invalid("Invalid CNPJ".to_string()); } - let weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let sum2: u32 = digits_vec[0..13] .iter() @@ -931,7 +823,6 @@ fn validate_cnpj(input: &str) -> ValidationResult { return ValidationResult::invalid("Invalid CNPJ".to_string()); } - let formatted = format!( "{}.{}.{}/{}-{}", &digits[0..2], @@ -948,9 +839,8 @@ fn validate_cnpj(input: &str) -> ValidationResult { } fn validate_url(input: &str) -> ValidationResult { - let url_str = if !input.starts_with("http://") && !input.starts_with("https://") { - format!("https://{}", input) + format!("https://{input}") } else { input.to_string() }; @@ -976,7 +866,6 @@ fn validate_uuid(input: &str) -> ValidationResult { fn validate_color(input: &str) -> ValidationResult { let lower = input.to_lowercase().trim().to_string(); - let named_colors = [ ("red", "#FF0000"), ("green", "#00FF00"), @@ -1003,7 +892,6 @@ fn validate_color(input: &str) -> ValidationResult { } } - let hex_regex = Regex::new(r"^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").unwrap(); if let Some(caps) = hex_regex.captures(&lower) { let hex = caps[1].to_uppercase(); @@ -1017,7 +905,6 @@ fn validate_color(input: &str) -> ValidationResult { return ValidationResult::valid(format!("#{}", full_hex)); } - let rgb_regex = Regex::new(r"^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$").unwrap(); if let Some(caps) = rgb_regex.captures(&lower) { @@ -1031,14 +918,12 @@ fn validate_color(input: &str) -> ValidationResult { } fn validate_credit_card(input: &str) -> ValidationResult { - let digits: String = input.chars().filter(|c| c.is_ascii_digit()).collect(); if digits.len() < 13 || digits.len() > 19 { return ValidationResult::invalid(InputType::CreditCard.error_message()); } - let mut sum = 0; let mut double = false; @@ -1058,7 +943,6 @@ fn validate_credit_card(input: &str) -> ValidationResult { return ValidationResult::invalid("Invalid card number".to_string()); } - let card_type = if digits.starts_with('4') { "Visa" } else if digits.starts_with("51") @@ -1078,7 +962,6 @@ fn validate_credit_card(input: &str) -> ValidationResult { "Unknown" }; - let masked = format!( "{} **** **** {}", &digits[0..4], @@ -1113,7 +996,6 @@ fn validate_password(input: &str) -> ValidationResult { _ => "weak", }; - ValidationResult::valid_with_metadata( "[PASSWORD SET]".to_string(), serde_json::json!({ @@ -1126,7 +1008,6 @@ fn validate_password(input: &str) -> ValidationResult { fn validate_menu(input: &str, options: &[String]) -> ValidationResult { let lower_input = input.to_lowercase().trim().to_string(); - for (i, opt) in options.iter().enumerate() { if opt.to_lowercase() == lower_input { return ValidationResult::valid_with_metadata( @@ -1136,7 +1017,6 @@ fn validate_menu(input: &str, options: &[String]) -> ValidationResult { } } - if let Ok(num) = lower_input.parse::() { if num >= 1 && num <= options.len() { let selected = &options[num - 1]; @@ -1147,7 +1027,6 @@ fn validate_menu(input: &str, options: &[String]) -> ValidationResult { } } - let matches: Vec<&String> = options .iter() .filter(|opt| opt.to_lowercase().contains(&lower_input)) @@ -1161,11 +1040,10 @@ fn validate_menu(input: &str, options: &[String]) -> ValidationResult { ); } - ValidationResult::invalid(format!("Please select one of: {}", options.join(", "))) + let opts = options.join(", "); + ValidationResult::invalid(format!("Please select one of: {opts}")) } - - pub async fn execute_talk( state: Arc, user_session: UserSession, @@ -1173,7 +1051,6 @@ pub async fn execute_talk( ) -> Result> { let mut suggestions = Vec::new(); - if let Some(redis_client) = &state.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { let redis_key = format!("suggestions:{}:{}", user_session.user_id, user_session.id); @@ -1212,7 +1089,6 @@ pub async fn execute_talk( let user_id = user_session.id.to_string(); let response_clone = response.clone(); - let web_adapter = Arc::clone(&state.web_adapter); tokio::spawn(async move { if let Err(e) = web_adapter @@ -1249,9 +1125,6 @@ pub fn talk_keyword(state: Arc, user: UserSession, engine: &mut Engine .unwrap(); } - - - pub async fn process_hear_input( state: &AppState, session_id: Uuid, @@ -1259,10 +1132,9 @@ pub async fn process_hear_input( input: &str, attachments: Option>, ) -> Result<(String, Option), String> { - let wait_data = if let Some(redis_client) = &state.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - let key = format!("hear:{}:{}", session_id, variable_name); + let key = format!("hear:{session_id}:{variable_name}"); let data: Result = redis::cmd("GET").arg(&key).query_async(&mut conn).await; @@ -1293,14 +1165,12 @@ pub async fn process_hear_input( .collect::>() }); - let validation_type = if let Some(opts) = options { InputType::Menu(opts) } else { InputType::from_str(input_type) }; - match validation_type { InputType::Image | InputType::QrCode => { if let Some(atts) = &attachments { @@ -1309,7 +1179,6 @@ pub async fn process_hear_input( .find(|a| a.mime_type.as_deref().unwrap_or("").starts_with("image/")) { if validation_type == InputType::QrCode { - return process_qrcode(state, &img.url).await; } return Ok(( @@ -1326,7 +1195,6 @@ pub async fn process_hear_input( .iter() .find(|a| a.mime_type.as_deref().unwrap_or("").starts_with("audio/")) { - return process_audio_to_text(state, &audio.url).await; } } @@ -1338,7 +1206,6 @@ pub async fn process_hear_input( .iter() .find(|a| a.mime_type.as_deref().unwrap_or("").starts_with("video/")) { - return process_video_description(state, &video.url).await; } } @@ -1358,14 +1225,12 @@ pub async fn process_hear_input( _ => {} } - let result = validate_input(input, &validation_type); if result.is_valid { - if let Some(redis_client) = &state.cache { if let Ok(mut conn) = redis_client.get_multiplexed_async_connection().await { - let key = format!("hear:{}:{}", session_id, variable_name); + let key = format!("hear:{session_id}:{variable_name}"); let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; } } @@ -1378,12 +1243,10 @@ pub async fn process_hear_input( } } - async fn process_qrcode( state: &AppState, image_url: &str, ) -> Result<(String, Option), String> { - let botmodels_url = { let config_url = state.conn.get().ok().and_then(|mut conn| { use crate::shared::models::schema::bot_memories::dsl::*; @@ -1401,7 +1264,6 @@ async fn process_qrcode( let client = reqwest::Client::new(); - let image_data = client .get(image_url) .send() @@ -1409,11 +1271,10 @@ async fn process_qrcode( .map_err(|e| format!("Failed to download image: {}", e))? .bytes() .await - .map_err(|e| format!("Failed to read image: {}", e))?; - + .map_err(|e| format!("Failed to fetch image: {e}"))?; let response = client - .post(format!("{}/api/v1/vision/qrcode", botmodels_url)) + .post(format!("{botmodels_url}/api/v1/vision/qrcode")) .header("Content-Type", "application/octet-stream") .body(image_data.to_vec()) .send() @@ -1424,7 +1285,7 @@ async fn process_qrcode( let result: serde_json::Value = response .json() .await - .map_err(|e| format!("Failed to parse response: {}", e))?; + .map_err(|e| format!("Failed to read image: {e}"))?; if let Some(qr_data) = result.get("data").and_then(|d| d.as_str()) { Ok(( @@ -1442,7 +1303,6 @@ async fn process_qrcode( } } - async fn process_audio_to_text( _state: &AppState, audio_url: &str, @@ -1452,7 +1312,6 @@ async fn process_audio_to_text( let client = reqwest::Client::new(); - let audio_data = client .get(audio_url) .send() @@ -1460,11 +1319,10 @@ async fn process_audio_to_text( .map_err(|e| format!("Failed to download audio: {}", e))? .bytes() .await - .map_err(|e| format!("Failed to read audio: {}", e))?; - + .map_err(|e| format!("Failed to read audio: {e}"))?; let response = client - .post(format!("{}/api/v1/speech/to-text", botmodels_url)) + .post(format!("{botmodels_url}/api/v1/speech/to-text")) .header("Content-Type", "application/octet-stream") .body(audio_data.to_vec()) .send() @@ -1494,7 +1352,6 @@ async fn process_audio_to_text( } } - async fn process_video_description( _state: &AppState, video_url: &str, @@ -1504,7 +1361,6 @@ async fn process_video_description( let client = reqwest::Client::new(); - let video_data = client .get(video_url) .send() @@ -1512,16 +1368,15 @@ async fn process_video_description( .map_err(|e| format!("Failed to download video: {}", e))? .bytes() .await - .map_err(|e| format!("Failed to read video: {}", e))?; - + .map_err(|e| format!("Failed to fetch video: {e}"))?; let response = client - .post(format!("{}/api/v1/vision/describe-video", botmodels_url)) + .post(format!("{botmodels_url}/api/v1/vision/describe-video")) .header("Content-Type", "application/octet-stream") .body(video_data.to_vec()) .send() .await - .map_err(|e| format!("Failed to call botmodels: {}", e))?; + .map_err(|e| format!("Failed to read video: {e}"))?; if response.status().is_success() { let result: serde_json::Value = response diff --git a/src/basic/keywords/human_approval.rs b/src/basic/keywords/human_approval.rs index 01f6a8604..0ef75f5ab 100644 --- a/src/basic/keywords/human_approval.rs +++ b/src/basic/keywords/human_approval.rs @@ -1,57 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - use chrono::{DateTime, Duration, Utc}; use rhai::{Array, Dynamic, Engine, Map}; use serde::{Deserialize, Serialize}; @@ -59,10 +5,8 @@ use std::collections::HashMap; use tracing::info; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalRequest { - pub id: Uuid, pub bot_id: Uuid, @@ -106,11 +50,9 @@ pub struct ApprovalRequest { pub comments: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApprovalStatus { - Pending, Approved, @@ -132,7 +74,6 @@ impl Default for ApprovalStatus { } } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApprovalDecision { @@ -143,7 +84,6 @@ pub enum ApprovalDecision { RequestInfo, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ApprovalChannel { @@ -176,10 +116,8 @@ impl std::fmt::Display for ApprovalChannel { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalChain { - pub name: String, pub bot_id: Uuid, @@ -195,10 +133,8 @@ pub struct ApprovalChain { pub created_at: DateTime, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalLevel { - pub level: u32, pub channel: ApprovalChannel, @@ -216,10 +152,8 @@ pub struct ApprovalLevel { pub required_approvals: u32, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApprovalAuditEntry { - pub id: Uuid, pub request_id: Uuid, @@ -237,7 +171,6 @@ pub struct ApprovalAuditEntry { pub user_agent: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum AuditAction { @@ -253,10 +186,8 @@ pub enum AuditAction { ContextUpdated, } - #[derive(Debug, Clone)] pub struct ApprovalConfig { - pub enabled: bool, pub default_timeout: u64, @@ -289,19 +220,16 @@ impl Default for ApprovalConfig { } } - #[derive(Debug)] pub struct ApprovalManager { config: ApprovalConfig, } impl ApprovalManager { - pub fn new(config: ApprovalConfig) -> Self { ApprovalManager { config } } - pub fn from_config(config_map: &HashMap) -> Self { let config = ApprovalConfig { enabled: config_map @@ -331,7 +259,6 @@ impl ApprovalManager { ApprovalManager::new(config) } - pub fn create_request( &self, bot_id: Uuid, @@ -373,12 +300,10 @@ impl ApprovalManager { } } - pub fn is_expired(&self, request: &ApprovalRequest) -> bool { Utc::now() > request.expires_at } - pub fn should_send_reminder(&self, request: &ApprovalRequest) -> bool { if request.status != ApprovalStatus::Pending { return false; @@ -398,7 +323,6 @@ impl ApprovalManager { since_last.num_seconds() >= self.config.reminder_interval as i64 } - pub fn generate_approval_url(&self, request_id: Uuid, action: &str, token: &str) -> String { let base_url = self .config @@ -412,7 +336,6 @@ impl ApprovalManager { ) } - pub fn generate_email_content(&self, request: &ApprovalRequest, token: &str) -> EmailContent { let approve_url = self.generate_approval_url(request.id, "approve", token); let reject_url = self.generate_approval_url(request.id, "reject", token); @@ -423,7 +346,7 @@ impl ApprovalManager { ); let body = format!( - r#" + r" An approval is requested for: Type: {} @@ -438,7 +361,7 @@ To approve, click: {} To reject, click: {} If you have questions, reply to this email. -"#, +", request.approval_type, request.message, serde_json::to_string_pretty(&request.context).unwrap_or_default(), @@ -454,7 +377,6 @@ If you have questions, reply to this email. } } - pub fn process_decision( &self, request: &mut ApprovalRequest, @@ -475,7 +397,6 @@ If you have questions, reply to this email. }; } - pub fn handle_timeout(&self, request: &mut ApprovalRequest) { if let Some(default_action) = &request.default_action { request.decision = Some(default_action.clone()); @@ -491,14 +412,11 @@ If you have questions, reply to this email. } } - pub fn evaluate_condition( &self, condition: &str, context: &serde_json::Value, ) -> Result { - - let parts: Vec<&str> = condition.split_whitespace().collect(); if parts.len() != 3 { return Err(format!("Invalid condition format: {}", condition)); @@ -531,7 +449,6 @@ If you have questions, reply to this email. } } - #[derive(Debug, Clone)] pub struct EmailContent { pub subject: String, @@ -539,7 +456,6 @@ pub struct EmailContent { pub html_body: Option, } - impl ApprovalRequest { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -589,7 +505,6 @@ impl ApprovalRequest { } } - fn json_to_dynamic(value: &serde_json::Value) -> Dynamic { match value { serde_json::Value::Null => Dynamic::UNIT, @@ -618,10 +533,7 @@ fn json_to_dynamic(value: &serde_json::Value) -> Dynamic { } } - pub fn register_approval_keywords(engine: &mut Engine) { - - engine.register_fn("approval_is_approved", |request: Map| -> bool { request .get("status") @@ -678,7 +590,6 @@ pub fn register_approval_keywords(engine: &mut Engine) { info!("Approval keywords registered"); } - pub const APPROVAL_SCHEMA: &str = r#" -- Approval requests CREATE TABLE IF NOT EXISTS approval_requests ( @@ -758,9 +669,8 @@ CREATE INDEX IF NOT EXISTS idx_approval_tokens_token ON approval_tokens(token); CREATE INDEX IF NOT EXISTS idx_approval_tokens_request_id ON approval_tokens(request_id); "#; - pub mod sql { - pub const INSERT_REQUEST: &str = r#" + pub const INSERT_REQUEST: &str = r" INSERT INTO approval_requests ( id, bot_id, session_id, initiated_by, approval_type, status, channel, recipient, context, message, timeout_seconds, @@ -769,9 +679,9 @@ pub mod sql { ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) - "#; + "; - pub const UPDATE_REQUEST: &str = r#" + pub const UPDATE_REQUEST: &str = r" UPDATE approval_requests SET status = $2, decision = $3, @@ -779,71 +689,71 @@ pub mod sql { decided_at = $5, comments = $6 WHERE id = $1 - "#; + "; - pub const GET_REQUEST: &str = r#" + pub const GET_REQUEST: &str = r" SELECT * FROM approval_requests WHERE id = $1 - "#; + "; - pub const GET_PENDING_REQUESTS: &str = r#" + pub const GET_PENDING_REQUESTS: &str = r" SELECT * FROM approval_requests WHERE status = 'pending' AND expires_at > NOW() ORDER BY created_at ASC - "#; + "; - pub const GET_EXPIRED_REQUESTS: &str = r#" + pub const GET_EXPIRED_REQUESTS: &str = r" SELECT * FROM approval_requests WHERE status = 'pending' AND expires_at <= NOW() - "#; + "; - pub const GET_REQUESTS_BY_SESSION: &str = r#" + pub const GET_REQUESTS_BY_SESSION: &str = r" SELECT * FROM approval_requests WHERE session_id = $1 ORDER BY created_at DESC - "#; + "; - pub const UPDATE_REMINDERS: &str = r#" + pub const UPDATE_REMINDERS: &str = r" UPDATE approval_requests SET reminders_sent = reminders_sent || $2::jsonb WHERE id = $1 - "#; + "; - pub const INSERT_AUDIT: &str = r#" + pub const INSERT_AUDIT: &str = r" INSERT INTO approval_audit_log ( id, request_id, action, actor, details, timestamp, ip_address, user_agent ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8 ) - "#; + "; - pub const GET_AUDIT_LOG: &str = r#" + pub const GET_AUDIT_LOG: &str = r" SELECT * FROM approval_audit_log WHERE request_id = $1 ORDER BY timestamp ASC - "#; + "; - pub const INSERT_TOKEN: &str = r#" + pub const INSERT_TOKEN: &str = r" INSERT INTO approval_tokens ( id, request_id, token, action, expires_at, created_at ) VALUES ( $1, $2, $3, $4, $5, $6 ) - "#; + "; - pub const GET_TOKEN: &str = r#" + pub const GET_TOKEN: &str = r" SELECT * FROM approval_tokens WHERE token = $1 AND used = false AND expires_at > NOW() - "#; + "; - pub const USE_TOKEN: &str = r#" + pub const USE_TOKEN: &str = r" UPDATE approval_tokens SET used = true, used_at = NOW() WHERE token = $1 - "#; + "; - pub const INSERT_CHAIN: &str = r#" + pub const INSERT_CHAIN: &str = r" INSERT INTO approval_chains ( id, name, bot_id, levels, stop_on_reject, require_all, description, created_at ) VALUES ( @@ -855,10 +765,10 @@ pub mod sql { stop_on_reject = $5, require_all = $6, description = $7 - "#; + "; - pub const GET_CHAIN: &str = r#" + pub const GET_CHAIN: &str = r" SELECT * FROM approval_chains WHERE bot_id = $1 AND name = $2 - "#; + "; } diff --git a/src/basic/keywords/knowledge_graph.rs b/src/basic/keywords/knowledge_graph.rs index 5287813f4..d7f78aa0b 100644 --- a/src/basic/keywords/knowledge_graph.rs +++ b/src/basic/keywords/knowledge_graph.rs @@ -1,50 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - use chrono::{DateTime, Utc}; use rhai::{Array, Dynamic, Engine, Map}; use serde::{Deserialize, Serialize}; @@ -52,10 +5,8 @@ use std::collections::HashMap; use tracing::info; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KgEntity { - pub id: Uuid, pub bot_id: Uuid, @@ -77,7 +28,6 @@ pub struct KgEntity { pub updated_at: DateTime, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum EntitySource { @@ -93,10 +43,8 @@ impl Default for EntitySource { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KgRelationship { - pub id: Uuid, pub bot_id: Uuid, @@ -118,10 +66,8 @@ pub struct KgRelationship { pub created_at: DateTime, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtractedEntity { - pub name: String, pub canonical_name: String, @@ -137,10 +83,8 @@ pub struct ExtractedEntity { pub properties: serde_json::Value, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtractedRelationship { - pub from_entity: String, pub to_entity: String, @@ -152,10 +96,8 @@ pub struct ExtractedRelationship { pub evidence: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtractionResult { - pub entities: Vec, pub relationships: Vec, @@ -163,10 +105,8 @@ pub struct ExtractionResult { pub metadata: ExtractionMetadata, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtractionMetadata { - pub model: String, pub processing_time_ms: u64, @@ -176,10 +116,8 @@ pub struct ExtractionMetadata { pub text_length: usize, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GraphQueryResult { - pub entities: Vec, pub relationships: Vec, @@ -189,10 +127,8 @@ pub struct GraphQueryResult { pub confidence: f64, } - #[derive(Debug, Clone)] pub struct KnowledgeGraphConfig { - pub enabled: bool, pub backend: String, @@ -233,19 +169,16 @@ impl Default for KnowledgeGraphConfig { } } - #[derive(Debug)] pub struct KnowledgeGraphManager { config: KnowledgeGraphConfig, } impl KnowledgeGraphManager { - pub fn new(config: KnowledgeGraphConfig) -> Self { KnowledgeGraphManager { config } } - pub fn from_config(config_map: &HashMap) -> Self { let config = KnowledgeGraphConfig { enabled: config_map @@ -284,7 +217,6 @@ impl KnowledgeGraphManager { KnowledgeGraphManager::new(config) } - pub fn generate_extraction_prompt(&self, text: &str) -> String { let entity_types = self.config.entity_types.join(", "); @@ -320,7 +252,6 @@ Respond with valid JSON only: ) } - pub fn generate_query_prompt(&self, query: &str, context: &str) -> String { format!( r#"Answer this question using the knowledge graph context. @@ -336,7 +267,6 @@ If the information is not available, say so clearly. ) } - pub fn parse_extraction_response( &self, response: &str, @@ -401,12 +331,10 @@ If the information is not available, say so clearly. }) } - pub fn should_extract(&self) -> bool { self.config.enabled && self.config.extract_entities } - pub fn is_valid_entity_type(&self, entity_type: &str) -> bool { self.config .entity_types @@ -415,16 +343,13 @@ If the information is not available, say so clearly. } } - fn extract_json(response: &str) -> Result { - if let Some(start) = response.find("```json") { if let Some(end) = response[start + 7..].find("```") { return Ok(response[start + 7..start + 7 + end].trim().to_string()); } } - if let Some(start) = response.find("```") { let after_start = start + 3; let json_start = response[after_start..] @@ -436,7 +361,6 @@ fn extract_json(response: &str) -> Result { } } - if let Some(start) = response.find('{') { if let Some(end) = response.rfind('}') { if end > start { @@ -448,7 +372,6 @@ fn extract_json(response: &str) -> Result { Err("No JSON found in response".to_string()) } - impl KgEntity { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -478,7 +401,6 @@ impl KgEntity { } } - impl KgRelationship { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -507,7 +429,6 @@ impl KgRelationship { } } - fn json_to_dynamic(value: &serde_json::Value) -> Dynamic { match value { serde_json::Value::Null => Dynamic::UNIT, @@ -536,10 +457,7 @@ fn json_to_dynamic(value: &serde_json::Value) -> Dynamic { } } - pub fn register_knowledge_graph_keywords(engine: &mut Engine) { - - engine.register_fn("entity_name", |entity: Map| -> String { entity .get("entity_name") @@ -576,7 +494,6 @@ pub fn register_knowledge_graph_keywords(engine: &mut Engine) { info!("Knowledge graph keywords registered"); } - pub const KNOWLEDGE_GRAPH_SCHEMA: &str = r#" -- Knowledge graph entities CREATE TABLE IF NOT EXISTS kg_entities ( @@ -627,9 +544,8 @@ CREATE INDEX IF NOT EXISTS idx_kg_entities_name_fts ON kg_entities USING GIN(to_tsvector('english', entity_name)); "#; - pub mod sql { - pub const INSERT_ENTITY: &str = r#" + pub const INSERT_ENTITY: &str = r" INSERT INTO kg_entities ( id, bot_id, entity_type, entity_name, aliases, properties, confidence, source, created_at, updated_at @@ -643,9 +559,9 @@ pub mod sql { confidence = GREATEST(kg_entities.confidence, $7), updated_at = $10 RETURNING id - "#; + "; - pub const INSERT_RELATIONSHIP: &str = r#" + pub const INSERT_RELATIONSHIP: &str = r" INSERT INTO kg_relationships ( id, bot_id, from_entity_id, to_entity_id, relationship_type, properties, confidence, bidirectional, source, created_at @@ -657,9 +573,9 @@ pub mod sql { properties = kg_relationships.properties || $6, confidence = GREATEST(kg_relationships.confidence, $7) RETURNING id - "#; + "; - pub const GET_ENTITY_BY_NAME: &str = r#" + pub const GET_ENTITY_BY_NAME: &str = r" SELECT * FROM kg_entities WHERE bot_id = $1 AND ( @@ -667,13 +583,13 @@ pub mod sql { OR aliases @> $3::jsonb ) LIMIT 1 - "#; + "; - pub const GET_ENTITY_BY_ID: &str = r#" + pub const GET_ENTITY_BY_ID: &str = r" SELECT * FROM kg_entities WHERE id = $1 - "#; + "; - pub const SEARCH_ENTITIES: &str = r#" + pub const SEARCH_ENTITIES: &str = r" SELECT * FROM kg_entities WHERE bot_id = $1 AND ( @@ -682,16 +598,16 @@ pub mod sql { ) ORDER BY confidence DESC LIMIT $4 - "#; + "; - pub const GET_ENTITIES_BY_TYPE: &str = r#" + pub const GET_ENTITIES_BY_TYPE: &str = r" SELECT * FROM kg_entities WHERE bot_id = $1 AND entity_type = $2 ORDER BY entity_name LIMIT $3 - "#; + "; - pub const GET_RELATED_ENTITIES: &str = r#" + pub const GET_RELATED_ENTITIES: &str = r" SELECT e.*, r.relationship_type, r.confidence as rel_confidence FROM kg_entities e JOIN kg_relationships r ON ( @@ -701,9 +617,9 @@ pub mod sql { WHERE r.bot_id = $2 ORDER BY r.confidence DESC LIMIT $3 - "#; + "; - pub const GET_RELATED_BY_TYPE: &str = r#" + pub const GET_RELATED_BY_TYPE: &str = r" SELECT e.*, r.relationship_type, r.confidence as rel_confidence FROM kg_entities e JOIN kg_relationships r ON ( @@ -713,17 +629,17 @@ pub mod sql { WHERE r.bot_id = $2 AND r.relationship_type = $3 ORDER BY r.confidence DESC LIMIT $4 - "#; + "; - pub const GET_RELATIONSHIP: &str = r#" + pub const GET_RELATIONSHIP: &str = r" SELECT * FROM kg_relationships WHERE bot_id = $1 AND from_entity_id = $2 AND to_entity_id = $3 AND relationship_type = $4 - "#; + "; - pub const GET_ALL_RELATIONSHIPS_FOR_ENTITY: &str = r#" + pub const GET_ALL_RELATIONSHIPS_FOR_ENTITY: &str = r" SELECT r.*, e1.entity_name as from_name, e1.entity_type as from_type, e2.entity_name as to_name, e2.entity_type as to_type @@ -733,42 +649,41 @@ pub mod sql { WHERE r.bot_id = $1 AND (r.from_entity_id = $2 OR r.to_entity_id = $2) ORDER BY r.confidence DESC - "#; + "; - pub const DELETE_ENTITY: &str = r#" + pub const DELETE_ENTITY: &str = r" DELETE FROM kg_entities WHERE id = $1 AND bot_id = $2 - "#; + "; - pub const DELETE_RELATIONSHIP: &str = r#" + pub const DELETE_RELATIONSHIP: &str = r" DELETE FROM kg_relationships WHERE id = $1 AND bot_id = $2 - "#; + "; - pub const COUNT_ENTITIES: &str = r#" + pub const COUNT_ENTITIES: &str = r" SELECT COUNT(*) FROM kg_entities WHERE bot_id = $1 - "#; + "; - pub const COUNT_RELATIONSHIPS: &str = r#" + pub const COUNT_RELATIONSHIPS: &str = r" SELECT COUNT(*) FROM kg_relationships WHERE bot_id = $1 - "#; + "; - pub const GET_ENTITY_TYPES: &str = r#" + pub const GET_ENTITY_TYPES: &str = r" SELECT DISTINCT entity_type, COUNT(*) as count FROM kg_entities WHERE bot_id = $1 GROUP BY entity_type ORDER BY count DESC - "#; + "; - pub const GET_RELATIONSHIP_TYPES: &str = r#" + pub const GET_RELATIONSHIP_TYPES: &str = r" SELECT DISTINCT relationship_type, COUNT(*) as count FROM kg_relationships WHERE bot_id = $1 GROUP BY relationship_type ORDER BY count DESC - "#; + "; - - pub const FIND_PATH: &str = r#" + pub const FIND_PATH: &str = r" WITH RECURSIVE path_finder AS ( -- Base case: start from source entity SELECT @@ -799,10 +714,9 @@ pub mod sql { WHERE to_entity_id = $3 ORDER BY depth LIMIT 1 - "#; + "; } - pub mod relationship_types { pub const WORKS_ON: &str = "works_on"; pub const REPORTS_TO: &str = "reports_to"; @@ -820,7 +734,6 @@ pub mod relationship_types { pub const ALIAS_OF: &str = "alias_of"; } - pub mod entity_types { pub const PERSON: &str = "person"; pub const ORGANIZATION: &str = "organization"; diff --git a/src/basic/keywords/last.rs b/src/basic/keywords/last.rs index e1e1fb068..a7f5d9e2f 100644 --- a/src/basic/keywords/last.rs +++ b/src/basic/keywords/last.rs @@ -3,7 +3,7 @@ use rhai::Engine; pub fn last_keyword(engine: &mut Engine) { engine - .register_custom_syntax(&["LAST", "(", "$expr$", ")"], false, { + .register_custom_syntax(["LAST", "(", "$expr$", ")"], false, { move |context, inputs| { let input_string = context.eval_expression_tree(&inputs[0])?; let input_str = input_string.to_string(); diff --git a/src/basic/keywords/llm_macros.rs b/src/basic/keywords/llm_macros.rs index 4f7a7e03d..5f7cd0dbd 100644 --- a/src/basic/keywords/llm_macros.rs +++ b/src/basic/keywords/llm_macros.rs @@ -28,14 +28,6 @@ | | \*****************************************************************************/ - - - - - - - - use crate::core::config::ConfigManager; use crate::shared::models::UserSession; use crate::shared::state::AppState; @@ -45,7 +37,6 @@ use std::sync::Arc; use std::time::Duration; use uuid::Uuid; - pub fn register_llm_macros(state: Arc, user: UserSession, engine: &mut Engine) { register_calculate_keyword(state.clone(), user.clone(), engine); register_validate_keyword(state.clone(), user.clone(), engine); @@ -53,7 +44,6 @@ pub fn register_llm_macros(state: Arc, user: UserSession, engine: &mut register_summarize_keyword(state, user, engine); } - async fn call_llm( state: &AppState, prompt: &str, @@ -75,7 +65,6 @@ async fn call_llm( Ok(processed) } - fn run_llm_with_timeout( state: Arc, prompt: String, @@ -117,8 +106,6 @@ fn run_llm_with_timeout( } } - - pub fn register_calculate_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); @@ -158,7 +145,7 @@ fn build_calculate_prompt(formula: &str, variables: &Dynamic) -> String { }; format!( - r#"You are a precise calculator. Evaluate the following expression. + r"You are a precise calculator. Evaluate the following expression. Formula: {} Variables: {} @@ -169,7 +156,7 @@ Instructions: 3. Return ONLY the final result (number, boolean, or text) 4. No explanations, just the result -Result:"#, +Result:", formula, vars_str ) } @@ -198,8 +185,6 @@ fn parse_calculate_result(result: &str) -> Result, _user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); @@ -306,8 +291,6 @@ fn parse_validate_result(result: &str) -> Result, _user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); @@ -336,20 +319,18 @@ pub fn register_translate_keyword(state: Arc, _user: UserSession, engi fn build_translate_prompt(text: &str, language: &str) -> String { format!( - r#"Translate the following text to {}. + r"Translate the following text to {}. Text: {} Return ONLY the translated text, no explanations: -Translation:"#, +Translation:", language, text ) } - - pub fn register_summarize_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); @@ -369,14 +350,14 @@ pub fn register_summarize_keyword(state: Arc, _user: UserSession, engi fn build_summarize_prompt(text: &str) -> String { format!( - r#"Summarize the following text concisely. + r"Summarize the following text concisely. Text: {} Return ONLY the summary: -Summary:"#, +Summary:", text ) } diff --git a/src/basic/keywords/on_change.rs b/src/basic/keywords/on_change.rs index 82c2d7a2c..d6e8cf59b 100644 --- a/src/basic/keywords/on_change.rs +++ b/src/basic/keywords/on_change.rs @@ -126,7 +126,7 @@ pub fn parse_folder_path(path: &str) -> (FolderProvider, Option, String) (FolderProvider::Local, None, path.to_string()) } -fn detect_provider_from_email(email: &str) -> FolderProvider { +pub fn detect_provider_from_email(email: &str) -> FolderProvider { let lower = email.to_lowercase(); if lower.ends_with("@gmail.com") || lower.contains("google") { FolderProvider::GDrive @@ -285,7 +285,7 @@ fn register_on_change_with_events(state: &AppState, user: UserSession, engine: & .unwrap(); } -fn sanitize_path_for_filename(path: &str) -> String { +pub fn sanitize_path_for_filename(path: &str) -> String { path.replace('/', "_") .replace('\\', "_") .replace(':', "_") diff --git a/src/basic/keywords/play.rs b/src/basic/keywords/play.rs index 5d650d69b..611e0b859 100644 --- a/src/basic/keywords/play.rs +++ b/src/basic/keywords/play.rs @@ -1,16 +1,3 @@ - - - - - - - - - - - - - use crate::shared::models::UserSession; use crate::shared::state::AppState; use log::{info, trace}; @@ -21,7 +8,6 @@ use std::path::Path; use std::sync::Arc; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ContentType { Video, @@ -39,10 +25,8 @@ pub enum ContentType { } impl ContentType { - pub fn from_extension(ext: &str) -> Self { match ext.to_lowercase().as_str() { - "mp4" | "webm" | "ogg" | "mov" | "avi" | "mkv" | "m4v" => ContentType::Video, "mp3" | "wav" | "flac" | "aac" | "m4a" | "wma" => ContentType::Audio, @@ -68,7 +52,6 @@ impl ContentType { } } - pub fn from_mime(mime: &str) -> Self { if mime.starts_with("video/") { ContentType::Video @@ -97,7 +80,6 @@ impl ContentType { } } - pub fn player_component(&self) -> &'static str { match self { ContentType::Video => "video-player", @@ -116,7 +98,6 @@ impl ContentType { } } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct PlayOptions { pub autoplay: bool, @@ -137,7 +118,6 @@ pub struct PlayOptions { } impl PlayOptions { - pub fn from_string(options_str: &str) -> Self { let mut opts = PlayOptions::default(); opts.controls = true; @@ -151,7 +131,6 @@ impl PlayOptions { "nocontrols" => opts.controls = false, "linenumbers" => opts.line_numbers = Some(true), _ => { - if let Some((key, value)) = opt.split_once('=') { match key { "start" => opts.start_time = value.parse().ok(), @@ -177,7 +156,6 @@ impl PlayOptions { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlayContent { pub id: Uuid, @@ -189,7 +167,6 @@ pub struct PlayContent { pub created_at: chrono::DateTime, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlayResponse { pub player_id: Uuid, @@ -201,7 +178,6 @@ pub struct PlayResponse { pub metadata: HashMap, } - pub fn play_keyword(state: Arc, user: UserSession, engine: &mut Engine) { if let Err(e) = play_simple_keyword(state.clone(), user.clone(), engine) { log::error!("Failed to register PLAY keyword: {}", e); @@ -220,7 +196,6 @@ pub fn play_keyword(state: Arc, user: UserSession, engine: &mut Engine } } - fn play_simple_keyword( state: Arc, user: UserSession, @@ -269,7 +244,6 @@ fn play_simple_keyword( Ok(()) } - fn play_with_options_keyword( state: Arc, user: UserSession, @@ -334,7 +308,6 @@ fn play_with_options_keyword( Ok(()) } - fn stop_keyword( state: Arc, user: UserSession, @@ -373,7 +346,6 @@ fn stop_keyword( Ok(()) } - fn pause_keyword( state: Arc, user: UserSession, @@ -413,7 +385,6 @@ fn pause_keyword( Ok(()) } - fn resume_keyword( state: Arc, user: UserSession, @@ -453,34 +424,25 @@ fn resume_keyword( Ok(()) } - - - async fn execute_play( state: &AppState, session_id: Uuid, source: &str, options: PlayOptions, ) -> Result { - let content_type = detect_content_type(source); - let source_url = resolve_source_url(state, session_id, source).await?; - let metadata = get_content_metadata(state, &source_url, &content_type).await?; - let player_id = Uuid::new_v4(); - let title = metadata .get("title") .cloned() .unwrap_or_else(|| extract_title_from_source(source)); - let response = PlayResponse { player_id, content_type: content_type.clone(), @@ -491,7 +453,6 @@ async fn execute_play( metadata, }; - send_play_to_client(state, session_id, &response).await?; info!( @@ -502,11 +463,8 @@ async fn execute_play( Ok(response) } - -fn detect_content_type(source: &str) -> ContentType { - +pub fn detect_content_type(source: &str) -> ContentType { if source.starts_with("http://") || source.starts_with("https://") { - if source.contains("youtube.com") || source.contains("youtu.be") || source.contains("vimeo.com") @@ -514,7 +472,6 @@ fn detect_content_type(source: &str) -> ContentType { return ContentType::Video; } - if source.contains("imgur.com") || source.contains("unsplash.com") || source.contains("pexels.com") @@ -522,18 +479,15 @@ fn detect_content_type(source: &str) -> ContentType { return ContentType::Image; } - if let Some(path) = source.split('?').next() { if let Some(ext) = Path::new(path).extension() { return ContentType::from_extension(&ext.to_string_lossy()); } } - return ContentType::Iframe; } - if let Some(ext) = Path::new(source).extension() { return ContentType::from_extension(&ext.to_string_lossy()); } @@ -541,20 +495,16 @@ fn detect_content_type(source: &str) -> ContentType { ContentType::Unknown } - async fn resolve_source_url( _state: &AppState, session_id: Uuid, source: &str, ) -> Result { - if source.starts_with("http://") || source.starts_with("https://") { return Ok(source.to_string()); } - if source.starts_with("/") || source.contains(".gbdrive") { - let file_url = format!( "/api/drive/file/{}?session={}", urlencoding::encode(source), @@ -563,7 +513,6 @@ async fn resolve_source_url( return Ok(file_url); } - let file_url = format!( "/api/drive/file/{}?session={}", urlencoding::encode(source), @@ -573,7 +522,6 @@ async fn resolve_source_url( Ok(file_url) } - async fn get_content_metadata( _state: &AppState, source_url: &str, @@ -584,7 +532,6 @@ async fn get_content_metadata( metadata.insert("source".to_string(), source_url.to_string()); metadata.insert("type".to_string(), format!("{:?}", content_type)); - match content_type { ContentType::Video => { metadata.insert("player".to_string(), "html5".to_string()); @@ -613,9 +560,7 @@ async fn get_content_metadata( Ok(metadata) } - -fn extract_title_from_source(source: &str) -> String { - +pub fn extract_title_from_source(source: &str) -> String { let path = source.split('?').next().unwrap_or(source); Path::new(path) @@ -624,7 +569,6 @@ fn extract_title_from_source(source: &str) -> String { .unwrap_or_else(|| "Untitled".to_string()) } - async fn send_play_to_client( state: &AppState, session_id: Uuid, @@ -638,7 +582,6 @@ async fn send_play_to_client( let message_str = serde_json::to_string(&message).map_err(|e| format!("Failed to serialize: {}", e))?; - let bot_response = crate::shared::models::BotResponse { bot_id: String::new(), user_id: String::new(), @@ -663,7 +606,6 @@ async fn send_play_to_client( Ok(()) } - async fn send_player_command( state: &AppState, session_id: Uuid, @@ -677,7 +619,6 @@ async fn send_player_command( let message_str = serde_json::to_string(&message).map_err(|e| format!("Failed to serialize: {}", e))?; - let _ = state .web_adapter .send_message_to_session( diff --git a/src/basic/keywords/sms.rs b/src/basic/keywords/sms.rs index 5c3136518..6b29723d5 100644 --- a/src/basic/keywords/sms.rs +++ b/src/basic/keywords/sms.rs @@ -28,18 +28,6 @@ | | \*****************************************************************************/ - - - - - - - - - - - - use crate::core::config::ConfigManager; use crate::shared::models::UserSession; use crate::shared::state::AppState; @@ -49,7 +37,6 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; - #[derive(Debug, Clone, PartialEq)] pub enum SmsProvider { Twilio, @@ -59,7 +46,6 @@ pub enum SmsProvider { Custom(String), } - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SmsPriority { Low, @@ -108,7 +94,6 @@ impl From<&str> for SmsProvider { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SmsSendResult { pub success: bool, @@ -119,15 +104,12 @@ pub struct SmsSendResult { pub error: Option, } - pub fn register_sms_keywords(state: Arc, user: UserSession, engine: &mut Engine) { register_send_sms_keyword(state.clone(), user.clone(), engine); register_send_sms_with_third_arg_keyword(state.clone(), user.clone(), engine); register_send_sms_full_keyword(state, user, engine); } - - pub fn register_send_sms_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -211,9 +193,6 @@ pub fn register_send_sms_keyword(state: Arc, user: UserSession, engine .unwrap(); } - - - pub fn register_send_sms_with_third_arg_keyword( state: Arc, user: UserSession, @@ -231,7 +210,6 @@ pub fn register_send_sms_with_third_arg_keyword( let message = context.eval_expression_tree(&inputs[1])?.to_string(); let third_arg = context.eval_expression_tree(&inputs[2])?.to_string(); - let is_priority = matches!( third_arg.to_lowercase().as_str(), "low" | "normal" | "high" | "urgent" | "critical" @@ -319,8 +297,6 @@ pub fn register_send_sms_with_third_arg_keyword( .unwrap(); } - - pub fn register_send_sms_full_keyword( state: Arc, user: UserSession, @@ -414,7 +390,6 @@ pub fn register_send_sms_full_keyword( ) .unwrap(); - let state_clone2 = Arc::clone(&state); let user_clone2 = user.clone(); @@ -517,7 +492,6 @@ async fn execute_send_sms( let config_manager = ConfigManager::new(state.conn.clone()); let bot_id = user.bot_id; - let provider_name = match provider_override { Some(p) => p.to_string(), None => config_manager @@ -527,7 +501,6 @@ async fn execute_send_sms( let provider = SmsProvider::from(provider_name.as_str()); - let priority = match priority_override { Some(p) => SmsPriority::from(p), None => { @@ -538,10 +511,8 @@ async fn execute_send_sms( } }; - let normalized_phone = normalize_phone_number(phone); - if matches!(priority, SmsPriority::High | SmsPriority::Urgent) { info!( "High priority SMS to {}: priority={}", @@ -549,7 +520,6 @@ async fn execute_send_sms( ); } - let result = match provider { SmsProvider::Twilio => { send_via_twilio(state, &bot_id, &normalized_phone, message, &priority).await @@ -602,20 +572,16 @@ async fn execute_send_sms( } fn normalize_phone_number(phone: &str) -> String { - let has_plus = phone.starts_with('+'); let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect(); if has_plus { format!("+{}", digits) } else if digits.len() == 10 { - format!("+1{}", digits) } else if digits.len() == 11 && digits.starts_with('1') { - format!("+{}", digits) } else { - format!("+{}", digits) } } @@ -647,8 +613,6 @@ async fn send_via_twilio( account_sid ); - - let final_message = match priority { SmsPriority::Urgent => format!("[URGENT] {}", message), SmsPriority::High => format!("[HIGH] {}", message), @@ -699,22 +663,17 @@ async fn send_via_aws_sns( .get_config(bot_id, "aws-region", Some("us-east-1")) .unwrap_or_else(|_| "us-east-1".to_string()); - let client = reqwest::Client::new(); let url = format!("https://sns.{}.amazonaws.com/", region); - let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string(); let _date = ×tamp[..8]; - - let sms_type = match priority { SmsPriority::High | SmsPriority::Urgent => "Transactional", _ => "Promotional", }; - let params = [ ("Action", "Publish"), ("PhoneNumber", phone), @@ -725,8 +684,6 @@ async fn send_via_aws_sns( ("MessageAttributes.entry.1.Value.StringValue", sms_type), ]; - - let response = client .post(&url) .form(¶ms) @@ -774,7 +731,6 @@ async fn send_via_vonage( let client = reqwest::Client::new(); - let message_class = match priority { SmsPriority::Urgent => Some("0"), _ => None, @@ -798,25 +754,24 @@ async fn send_via_vonage( .send() .await?; - if response.status().is_success() { - let json: serde_json::Value = response.json().await?; - let messages = json["messages"].as_array(); - - if let Some(msgs) = messages { - if let Some(first) = msgs.first() { - if first["status"].as_str() == Some("0") { - return Ok(first["message-id"].as_str().map(|s| s.to_string())); - } else { - let error_text = first["error-text"].as_str().unwrap_or("Unknown error"); - return Err(format!("Vonage error: {}", error_text).into()); - } - } - } - Err("Invalid Vonage response".into()) - } else { + if !response.status().is_success() { let error_text = response.text().await?; - Err(format!("Vonage API error: {}", error_text).into()) + return Err(format!("Vonage API error: {error_text}").into()); } + + let json: serde_json::Value = response.json().await?; + let messages = json["messages"].as_array(); + + if let Some(msgs) = messages { + if let Some(first) = msgs.first() { + if first["status"].as_str() == Some("0") { + return Ok(first["message-id"].as_str().map(|s| s.to_string())); + } + let error_text = first["error-text"].as_str().unwrap_or("Unknown error"); + return Err(format!("Vonage error: {error_text}").into()); + } + } + Err("Invalid Vonage response".into()) } async fn send_via_messagebird( @@ -840,7 +795,6 @@ async fn send_via_messagebird( let client = reqwest::Client::new(); - let type_details = match priority { SmsPriority::Urgent => Some(serde_json::json!({"class": 0})), SmsPriority::High => Some(serde_json::json!({"class": 1})), diff --git a/src/basic/keywords/transfer_to_human.rs b/src/basic/keywords/transfer_to_human.rs index b6df5452f..7f0cda0d7 100644 --- a/src/basic/keywords/transfer_to_human.rs +++ b/src/basic/keywords/transfer_to_human.rs @@ -1,42 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - use crate::shared::models::UserSession; use crate::shared::state::AppState; use chrono::Utc; @@ -48,10 +9,8 @@ use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransferToHumanRequest { - pub name: Option, pub department: Option, @@ -63,7 +22,6 @@ pub struct TransferToHumanRequest { pub context: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransferResult { pub success: bool, @@ -75,11 +33,9 @@ pub struct TransferResult { pub message: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TransferStatus { - Queued, Assigned, @@ -95,7 +51,6 @@ pub enum TransferStatus { Error, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Attendant { pub id: String, @@ -107,7 +62,6 @@ pub struct Attendant { pub status: AttendantStatus, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum AttendantStatus { @@ -123,14 +77,12 @@ impl Default for AttendantStatus { } } - pub fn is_crm_enabled(bot_id: Uuid, work_path: &str) -> bool { let config_path = PathBuf::from(work_path) .join(format!("{}.gbai", bot_id)) .join("config.csv"); if !config_path.exists() { - let alt_path = PathBuf::from(work_path).join("config.csv"); if alt_path.exists() { return check_config_for_crm(&alt_path); @@ -167,14 +119,12 @@ fn check_config_for_crm(config_path: &PathBuf) -> bool { } } - pub fn read_attendants(bot_id: Uuid, work_path: &str) -> Vec { let attendant_path = PathBuf::from(work_path) .join(format!("{}.gbai", bot_id)) .join("attendant.csv"); if !attendant_path.exists() { - let alt_path = PathBuf::from(work_path).join("attendant.csv"); if alt_path.exists() { return parse_attendants_csv(&alt_path); @@ -192,11 +142,9 @@ fn parse_attendants_csv(path: &PathBuf) -> Vec { let mut attendants = Vec::new(); let mut lines = content.lines(); - let header = lines.next().unwrap_or(""); let headers: Vec = header.split(',').map(|s| s.trim().to_lowercase()).collect(); - let id_idx = headers.iter().position(|h| h == "id").unwrap_or(0); let name_idx = headers.iter().position(|h| h == "name").unwrap_or(1); let channel_idx = headers.iter().position(|h| h == "channel").unwrap_or(2); @@ -239,7 +187,6 @@ fn parse_attendants_csv(path: &PathBuf) -> Vec { } } - pub fn find_attendant<'a>( attendants: &'a [Attendant], name: Option<&str>, @@ -248,7 +195,6 @@ pub fn find_attendant<'a>( if let Some(search_name) = name { let search_lower = search_name.to_lowercase(); - if let Some(att) = attendants .iter() .find(|a| a.name.to_lowercase() == search_lower) @@ -256,7 +202,6 @@ pub fn find_attendant<'a>( return Some(att); } - if let Some(att) = attendants .iter() .find(|a| a.name.to_lowercase().contains(&search_lower)) @@ -264,7 +209,6 @@ pub fn find_attendant<'a>( return Some(att); } - if let Some(att) = attendants .iter() .find(|a| a.aliases.contains(&search_lower)) @@ -272,7 +216,6 @@ pub fn find_attendant<'a>( return Some(att); } - if let Some(att) = attendants .iter() .find(|a| a.id.to_lowercase() == search_lower) @@ -284,7 +227,6 @@ pub fn find_attendant<'a>( if let Some(dept) = department { let dept_lower = dept.to_lowercase(); - if let Some(att) = attendants.iter().find(|a| { a.department .as_ref() @@ -295,7 +237,6 @@ pub fn find_attendant<'a>( return Some(att); } - if let Some(att) = attendants.iter().find(|a| { a.preferences.to_lowercase().contains(&dept_lower) && a.status == AttendantStatus::Online @@ -304,13 +245,11 @@ pub fn find_attendant<'a>( } } - attendants .iter() .find(|a| a.status == AttendantStatus::Online) } - fn priority_to_int(priority: Option<&str>) -> i32 { match priority.map(|p| p.to_lowercase()).as_deref() { Some("urgent") => 3, @@ -321,7 +260,6 @@ fn priority_to_int(priority: Option<&str>) -> i32 { } } - pub async fn execute_transfer( state: Arc, session: &UserSession, @@ -330,7 +268,6 @@ pub async fn execute_transfer( let work_path = "./work"; let bot_id = session.bot_id; - if !is_crm_enabled(bot_id, work_path) { return TransferResult { success: false, @@ -344,7 +281,6 @@ pub async fn execute_transfer( }; } - let attendants = read_attendants(bot_id, work_path); if attendants.is_empty() { return TransferResult { @@ -359,14 +295,12 @@ pub async fn execute_transfer( }; } - let attendant = find_attendant( &attendants, request.name.as_deref(), request.department.as_deref(), ); - if request.name.is_some() && attendant.is_none() { return TransferResult { success: false, @@ -387,7 +321,6 @@ pub async fn execute_transfer( }; } - let priority = priority_to_int(request.priority.as_deref()); let transfer_context = serde_json::json!({ "transfer_requested_at": Utc::now().to_rfc3339(), @@ -402,7 +335,6 @@ pub async fn execute_transfer( "status": if attendant.is_some() { "assigned" } else { "queued" }, }); - let session_id = session.id; let conn = state.conn.clone(); let ctx_data = transfer_context.clone(); @@ -491,7 +423,6 @@ pub async fn execute_transfer( } } - impl TransferResult { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -532,11 +463,11 @@ async fn calculate_queue_position(state: &Arc, current_session_id: Uui }; let query = diesel::sql_query( - r#"SELECT COUNT(*) as position FROM user_sessions + r"SELECT COUNT(*) as position FROM user_sessions WHERE context_data->>'needs_human' = 'true' AND context_data->>'status' = 'queued' AND created_at <= (SELECT created_at FROM user_sessions WHERE id = $1) - AND id != $2"#, + AND id != $2", ) .bind::(current_session_id) .bind::(current_session_id); @@ -563,13 +494,11 @@ async fn calculate_queue_position(state: &Arc, current_session_id: Uui } } - pub fn register_transfer_to_human_keyword( state: Arc, user: UserSession, engine: &mut Engine, ) { - let state_clone = state.clone(); let user_clone = user.clone(); engine.register_fn("transfer_to_human", move || -> Dynamic { @@ -595,7 +524,6 @@ pub fn register_transfer_to_human_keyword( result.to_dynamic() }); - let state_clone = state.clone(); let user_clone = user.clone(); engine.register_fn("transfer_to_human", move |name: &str| -> Dynamic { @@ -622,7 +550,6 @@ pub fn register_transfer_to_human_keyword( result.to_dynamic() }); - let state_clone = state.clone(); let user_clone = user.clone(); engine.register_fn( @@ -653,7 +580,6 @@ pub fn register_transfer_to_human_keyword( }, ); - let state_clone = state.clone(); let user_clone = user.clone(); engine.register_fn( @@ -685,7 +611,6 @@ pub fn register_transfer_to_human_keyword( }, ); - let state_clone = state.clone(); let user_clone = user.clone(); engine.register_fn("transfer_to_human_ex", move |params: Map| -> Dynamic { @@ -730,7 +655,6 @@ pub fn register_transfer_to_human_keyword( debug!("Registered TRANSFER TO HUMAN keywords"); } - pub const TRANSFER_TO_HUMAN_TOOL_SCHEMA: &str = r#"{ "name": "transfer_to_human", "description": "Transfer the conversation to a human attendant. Use this when the customer explicitly asks to speak with a person, when the issue is too complex for automated handling, or when emotional support is needed.", @@ -761,7 +685,6 @@ pub const TRANSFER_TO_HUMAN_TOOL_SCHEMA: &str = r#"{ } }"#; - pub fn get_tool_definition() -> serde_json::Value { serde_json::json!({ "type": "function", diff --git a/src/basic/keywords/wait.rs b/src/basic/keywords/wait.rs index f358401e7..4afcc0ea8 100644 --- a/src/basic/keywords/wait.rs +++ b/src/basic/keywords/wait.rs @@ -6,7 +6,7 @@ use std::time::Duration; pub fn wait_keyword(_state: &AppState, _user: UserSession, engine: &mut Engine) { engine - .register_custom_syntax(&["WAIT", "$expr$"], false, move |context, inputs| { + .register_custom_syntax(["WAIT", "$expr$"], false, move |context, inputs| { let seconds = context.eval_expression_tree(&inputs[0])?; let duration_secs = if seconds.is::() { #[allow(clippy::cast_precision_loss)] diff --git a/src/basic/keywords/web_data.rs b/src/basic/keywords/web_data.rs index d508589cc..8631e2296 100644 --- a/src/basic/keywords/web_data.rs +++ b/src/basic/keywords/web_data.rs @@ -345,9 +345,7 @@ async fn scrape_table( .chain(row.select(&td_sel)) .map(|cell| cell.text().collect::>().join(" ").trim().to_string()) .collect(); - if headers.is_empty() { - continue; - } + if headers.is_empty() {} } else { let mut row_map = Map::new(); for (j, cell) in row.select(&td_sel).enumerate() { diff --git a/src/calendar/caldav.rs b/src/calendar/caldav.rs index 43046138b..4b2866828 100644 --- a/src/calendar/caldav.rs +++ b/src/calendar/caldav.rs @@ -1,8 +1,3 @@ - - - - - use axum::{ http::StatusCode, response::{IntoResponse, Response}, @@ -14,11 +9,7 @@ use std::sync::Arc; use super::CalendarEngine; use crate::shared::state::AppState; - - pub fn create_caldav_router(_engine: Arc) -> Router> { - - Router::new() .route("/caldav", get(caldav_root)) .route("/caldav/principals", get(caldav_principals)) @@ -30,7 +21,6 @@ pub fn create_caldav_router(_engine: Arc) -> Router impl IntoResponse { Response::builder() .status(StatusCode::OK) @@ -57,7 +47,6 @@ async fn caldav_root() -> impl IntoResponse { .unwrap() } - async fn caldav_principals() -> impl IntoResponse { Response::builder() .status(StatusCode::OK) @@ -86,7 +75,6 @@ async fn caldav_principals() -> impl IntoResponse { .unwrap() } - async fn caldav_calendars() -> impl IntoResponse { Response::builder() .status(StatusCode::OK) @@ -129,7 +117,6 @@ async fn caldav_calendars() -> impl IntoResponse { .unwrap() } - async fn caldav_calendar() -> impl IntoResponse { Response::builder() .status(StatusCode::OK) @@ -156,14 +143,12 @@ async fn caldav_calendar() -> impl IntoResponse { .unwrap() } - async fn caldav_event() -> impl IntoResponse { - Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/calendar; charset=utf-8") .body( - r#"BEGIN:VCALENDAR + r"BEGIN:VCALENDAR VERSION:2.0 PRODID:- BEGIN:VEVENT @@ -173,15 +158,13 @@ DTSTART:20240101T090000Z DTEND:20240101T100000Z SUMMARY:Placeholder Event END:VEVENT -END:VCALENDAR"# +END:VCALENDAR" .to_string(), ) .unwrap() } - async fn caldav_put_event() -> impl IntoResponse { - Response::builder() .status(StatusCode::CREATED) .header("ETag", "\"placeholder-etag\"") diff --git a/src/compliance/code_scanner.rs b/src/compliance/code_scanner.rs index f43aa0c13..76df817a8 100644 --- a/src/compliance/code_scanner.rs +++ b/src/compliance/code_scanner.rs @@ -1,16 +1,9 @@ - - - - - use chrono::{DateTime, Utc}; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::path::{Path, PathBuf}; use walkdir::WalkDir; - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "lowercase")] pub enum IssueSeverity { @@ -33,7 +26,6 @@ impl std::fmt::Display for IssueSeverity { } } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum IssueType { @@ -64,7 +56,6 @@ impl std::fmt::Display for IssueType { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CodeIssue { pub id: String, @@ -80,7 +71,6 @@ pub struct CodeIssue { pub detected_at: DateTime, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BotScanResult { pub bot_id: String, @@ -91,7 +81,6 @@ pub struct BotScanResult { pub stats: ScanStats, } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ScanStats { pub critical: usize, @@ -124,7 +113,6 @@ impl ScanStats { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplianceScanResult { pub scanned_at: DateTime, @@ -135,7 +123,6 @@ pub struct ComplianceScanResult { pub bot_results: Vec, } - struct ScanPattern { regex: Regex, issue_type: IssueType, @@ -146,14 +133,12 @@ struct ScanPattern { category: String, } - pub struct CodeScanner { patterns: Vec, base_path: PathBuf, } impl CodeScanner { - pub fn new(base_path: impl AsRef) -> Self { let patterns = Self::build_patterns(); Self { @@ -162,11 +147,9 @@ impl CodeScanner { } } - fn build_patterns() -> Vec { let mut patterns = Vec::new(); - patterns.push(ScanPattern { regex: Regex::new(r#"(?i)password\s*=\s*["'][^"']+["']"#).unwrap(), issue_type: IssueType::PasswordInConfig, @@ -197,9 +180,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)IF\s+.*\binput\b"#).unwrap(), + regex: Regex::new(r"(?i)IF\s+.*\binput\b").unwrap(), issue_type: IssueType::DeprecatedIfInput, severity: IssueSeverity::Medium, title: "Deprecated IF...input Pattern".to_string(), @@ -211,9 +193,8 @@ impl CodeScanner { category: "Code Quality".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)\b(GET_BOT_MEMORY|SET_BOT_MEMORY|GET_USER_MEMORY|SET_USER_MEMORY|USE_KB|USE_TOOL|SEND_MAIL|CREATE_TASK)\b"#).unwrap(), + regex: Regex::new(r"(?i)\b(GET_BOT_MEMORY|SET_BOT_MEMORY|GET_USER_MEMORY|SET_USER_MEMORY|USE_KB|USE_TOOL|SEND_MAIL|CREATE_TASK)\b").unwrap(), issue_type: IssueType::UnderscoreInKeyword, severity: IssueSeverity::Low, title: "Underscore in Keyword".to_string(), @@ -222,9 +203,8 @@ impl CodeScanner { category: "Naming Convention".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)POST\s+TO\s+INSTAGRAM\s+\w+\s*,\s*\w+"#).unwrap(), + regex: Regex::new(r"(?i)POST\s+TO\s+INSTAGRAM\s+\w+\s*,\s*\w+").unwrap(), issue_type: IssueType::InsecurePattern, severity: IssueSeverity::High, title: "Instagram Credentials in Code".to_string(), @@ -235,10 +215,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)(SELECT|INSERT|UPDATE|DELETE)\s+.*(FROM|INTO|SET)\s+"#) - .unwrap(), + regex: Regex::new(r"(?i)(SELECT|INSERT|UPDATE|DELETE)\s+.*(FROM|INTO|SET)\s+").unwrap(), issue_type: IssueType::FragileCode, severity: IssueSeverity::Medium, title: "Raw SQL Query".to_string(), @@ -250,9 +228,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)\bEVAL\s*\("#).unwrap(), + regex: Regex::new(r"(?i)\bEVAL\s*\(").unwrap(), issue_type: IssueType::FragileCode, severity: IssueSeverity::High, title: "Dynamic Code Execution".to_string(), @@ -261,7 +238,6 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { regex: Regex::new( r#"(?i)(password|secret|key|token)\s*=\s*["'][A-Za-z0-9+/=]{40,}["']"#, @@ -276,9 +252,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)(AKIA[0-9A-Z]{16})"#).unwrap(), + regex: Regex::new(r"(?i)(AKIA[0-9A-Z]{16})").unwrap(), issue_type: IssueType::HardcodedSecret, severity: IssueSeverity::Critical, title: "AWS Access Key".to_string(), @@ -288,9 +263,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----"#).unwrap(), + regex: Regex::new(r"-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----").unwrap(), issue_type: IssueType::HardcodedSecret, severity: IssueSeverity::Critical, title: "Private Key in Code".to_string(), @@ -300,9 +274,8 @@ impl CodeScanner { category: "Security".to_string(), }); - patterns.push(ScanPattern { - regex: Regex::new(r#"(?i)(postgres|mysql|mongodb|redis)://[^:]+:[^@]+@"#).unwrap(), + regex: Regex::new(r"(?i)(postgres|mysql|mongodb|redis)://[^:]+:[^@]+@").unwrap(), issue_type: IssueType::HardcodedSecret, severity: IssueSeverity::Critical, title: "Database Credentials in Connection String".to_string(), @@ -314,7 +287,6 @@ impl CodeScanner { patterns } - pub async fn scan_all( &self, ) -> Result> { @@ -323,13 +295,11 @@ impl CodeScanner { let mut total_stats = ScanStats::default(); let mut total_files = 0; - let templates_path = self.base_path.join("templates"); let work_path = self.base_path.join("work"); let mut bot_paths = Vec::new(); - if templates_path.exists() { for entry in WalkDir::new(&templates_path).max_depth(3) { if let Ok(entry) = entry { @@ -344,7 +314,6 @@ impl CodeScanner { } } - if work_path.exists() { for entry in WalkDir::new(&work_path).max_depth(3) { if let Ok(entry) = entry { @@ -359,7 +328,6 @@ impl CodeScanner { } } - for bot_path in &bot_paths { let result = self.scan_bot(bot_path).await?; total_files += result.files_scanned; @@ -379,7 +347,6 @@ impl CodeScanner { }) } - pub async fn scan_bot( &self, bot_path: &Path, @@ -397,7 +364,6 @@ impl CodeScanner { let mut stats = ScanStats::default(); let mut files_scanned = 0; - for entry in WalkDir::new(bot_path) { if let Ok(entry) = entry { let path = entry.path(); @@ -415,7 +381,6 @@ impl CodeScanner { } } - let config_path = bot_path.join("config.csv"); if config_path.exists() { let vault_configured = self.check_vault_config(&config_path).await?; @@ -438,7 +403,6 @@ impl CodeScanner { } } - issues.sort_by(|a, b| b.severity.cmp(&a.severity)); Ok(BotScanResult { @@ -451,7 +415,6 @@ impl CodeScanner { }) } - async fn scan_file( &self, file_path: &Path, @@ -468,7 +431,6 @@ impl CodeScanner { for (line_number, line) in content.lines().enumerate() { let line_num = line_number + 1; - let trimmed = line.trim(); if trimmed.starts_with("REM") || trimmed.starts_with("'") || trimmed.starts_with("//") { continue; @@ -476,7 +438,6 @@ impl CodeScanner { for pattern in &self.patterns { if pattern.regex.is_match(line) { - let snippet = self.redact_sensitive(line); let issue = CodeIssue { @@ -500,18 +461,15 @@ impl CodeScanner { Ok(issues) } - fn redact_sensitive(&self, line: &str) -> String { let mut result = line.to_string(); - let secret_pattern = Regex::new(r#"(["'])[^"']{8,}(["'])"#).unwrap(); result = secret_pattern .replace_all(&result, "$1***REDACTED***$2") .to_string(); - - let aws_pattern = Regex::new(r#"AKIA[0-9A-Z]{16}"#).unwrap(); + let aws_pattern = Regex::new(r"AKIA[0-9A-Z]{16}").unwrap(); result = aws_pattern .replace_all(&result, "AKIA***REDACTED***") .to_string(); @@ -519,14 +477,12 @@ impl CodeScanner { result } - async fn check_vault_config( &self, config_path: &Path, ) -> Result> { let content = tokio::fs::read_to_string(config_path).await?; - let has_vault = content.to_lowercase().contains("vault_addr") || content.to_lowercase().contains("vault_token") || content.to_lowercase().contains("vault-"); @@ -534,7 +490,6 @@ impl CodeScanner { Ok(has_vault) } - pub async fn scan_bots( &self, bot_ids: &[String], @@ -543,14 +498,11 @@ impl CodeScanner { return self.scan_all().await; } - - let mut full_result = self.scan_all().await?; full_result .bot_results .retain(|r| bot_ids.contains(&r.bot_id) || bot_ids.contains(&r.bot_name)); - let mut new_stats = ScanStats::default(); for bot in &full_result.bot_results { new_stats.merge(&bot.stats); @@ -562,11 +514,9 @@ impl CodeScanner { } } - pub struct ComplianceReporter; impl ComplianceReporter { - pub fn to_html(result: &ComplianceScanResult) -> String { let mut html = String::new(); @@ -617,12 +567,10 @@ impl ComplianceReporter { html } - pub fn to_json(result: &ComplianceScanResult) -> Result { serde_json::to_string_pretty(result) } - pub fn to_csv(result: &ComplianceScanResult) -> String { let mut csv = String::new(); csv.push_str("Severity,Type,Category,File,Line,Title,Description,Remediation\n"); @@ -650,7 +598,6 @@ impl ComplianceReporter { } } - fn escape_csv(s: &str) -> String { if s.contains(',') || s.contains('"') || s.contains('\n') { format!("\"{}\"", s.replace('"', "\"\"")) diff --git a/src/console/wizard.rs b/src/console/wizard.rs index 13c3d49d9..ae5b61df7 100644 --- a/src/console/wizard.rs +++ b/src/console/wizard.rs @@ -1,13 +1,3 @@ - - - - - - - - - - use crate::shared::platform_name; use crate::shared::BOTSERVER_VERSION; use crossterm::{ @@ -21,38 +11,27 @@ use serde::{Deserialize, Serialize}; use std::io::{self, Write}; use std::path::PathBuf; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WizardConfig { - pub llm_provider: LlmProvider, - pub llm_api_key: Option, - pub local_model_path: Option, - pub components: Vec, - pub admin: AdminConfig, - pub organization: OrgConfig, - pub template: Option, - pub install_mode: InstallMode, - pub data_dir: PathBuf, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum LlmProvider { Claude, @@ -74,7 +53,6 @@ impl std::fmt::Display for LlmProvider { } } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ComponentChoice { Drive, @@ -104,7 +82,6 @@ impl std::fmt::Display for ComponentChoice { } } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AdminConfig { pub username: String, @@ -113,7 +90,6 @@ pub struct AdminConfig { pub display_name: String, } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OrgConfig { pub name: String, @@ -121,7 +97,6 @@ pub struct OrgConfig { pub domain: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum InstallMode { Development, @@ -149,7 +124,6 @@ impl Default for WizardConfig { } } - #[derive(Debug)] pub struct StartupWizard { config: WizardConfig, @@ -166,12 +140,10 @@ impl StartupWizard { } } - pub fn run(&mut self) -> io::Result { terminal::enable_raw_mode()?; let mut stdout = io::stdout(); - execute!( stdout, terminal::Clear(ClearType::All), @@ -181,31 +153,24 @@ impl StartupWizard { self.show_welcome(&mut stdout)?; self.wait_for_enter()?; - self.current_step = 1; self.step_install_mode(&mut stdout)?; - self.current_step = 2; self.step_llm_provider(&mut stdout)?; - self.current_step = 3; self.step_components(&mut stdout)?; - self.current_step = 4; self.step_organization(&mut stdout)?; - self.current_step = 5; self.step_admin_user(&mut stdout)?; - self.current_step = 6; self.step_template(&mut stdout)?; - self.current_step = 7; self.step_summary(&mut stdout)?; @@ -220,7 +185,7 @@ impl StartupWizard { cursor::MoveTo(0, 0) )?; - let banner = r#" + let banner = r" ╔══════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗ ███████╗███╗ ██╗███████╗██████╗ █████╗ ██╗ ║ @@ -237,7 +202,7 @@ impl StartupWizard { ║ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ║ ║ ║ ╚══════════════════════════════════════════════════════════════════╝ -"#; +"; execute!( stdout, @@ -288,7 +253,6 @@ impl StartupWizard { cursor::MoveTo(0, 0) )?; - let progress = format!("Step {}/{}: {}", self.current_step, self.total_steps, title); let bar_width = 50; let filled = (self.current_step * bar_width) / self.total_steps; @@ -396,7 +360,6 @@ impl StartupWizard { let selected = self.select_option(stdout, &options, 0)?; self.config.llm_provider = options[selected].2.clone(); - if self.config.llm_provider != LlmProvider::Local && self.config.llm_provider != LlmProvider::None { @@ -485,7 +448,6 @@ impl StartupWizard { io::stdin().read_line(&mut org_name)?; self.config.organization.name = org_name.trim().to_string(); - self.config.organization.slug = self .config .organization @@ -557,7 +519,6 @@ impl StartupWizard { execute!(stdout, cursor::MoveTo(2, 13), Print("Admin password: "))?; stdout.flush()?; - let mut password = String::new(); io::stdin().read_line(&mut password)?; self.config.admin.password = password.trim().to_string(); @@ -824,7 +785,6 @@ impl StartupWizard { } KeyCode::Char(' ') => { if options[cursor].2 { - selected[cursor] = !selected[cursor]; } } @@ -857,7 +817,6 @@ impl StartupWizard { } } - pub fn save_wizard_config(config: &WizardConfig, path: &str) -> io::Result<()> { let content = toml::to_string_pretty(config) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; @@ -865,7 +824,6 @@ pub fn save_wizard_config(config: &WizardConfig, path: &str) -> io::Result<()> { Ok(()) } - pub fn load_wizard_config(path: &str) -> io::Result { let content = std::fs::read_to_string(path)?; let config: WizardConfig = @@ -873,32 +831,26 @@ pub fn load_wizard_config(path: &str) -> io::Result { Ok(config) } - pub fn should_run_wizard() -> bool { !std::path::Path::new("./botserver-stack").exists() && !std::path::Path::new("/opt/gbo").exists() } - pub fn apply_wizard_config(config: &WizardConfig) -> io::Result<()> { use std::fs; - fs::create_dir_all(&config.data_dir)?; - let subdirs = ["bots", "logs", "cache", "uploads", "config"]; for subdir in &subdirs { fs::create_dir_all(config.data_dir.join(subdir))?; } - save_wizard_config( config, &config.data_dir.join("config/wizard.toml").to_string_lossy(), )?; - let mut env_content = String::new(); env_content.push_str(&format!( "# Generated by {} Setup Wizard\n\n", diff --git a/src/core/bootstrap/mod.rs b/src/core/bootstrap/mod.rs index 7ea369eaf..2ed0820b0 100644 --- a/src/core/bootstrap/mod.rs +++ b/src/core/bootstrap/mod.rs @@ -28,7 +28,6 @@ pub struct BootstrapManager { } impl BootstrapManager { pub async fn new(mode: InstallMode, tenant: Option) -> Self { - let stack_path = std::env::var("BOTSERVER_STACK_PATH") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("./botserver-stack")); @@ -40,12 +39,10 @@ impl BootstrapManager { } } - fn stack_dir(&self, subpath: &str) -> PathBuf { self.stack_path.join(subpath) } - fn vault_bin(&self) -> String { self.stack_dir("bin/vault/vault") .to_str() @@ -53,12 +50,9 @@ impl BootstrapManager { .to_string() } - - pub fn kill_stack_processes() { info!("Killing any existing stack processes..."); - let patterns = vec![ "botserver-stack/bin/vault", "botserver-stack/bin/tables", @@ -77,7 +71,6 @@ impl BootstrapManager { let _ = Command::new("pkill").args(["-9", "-f", pattern]).output(); } - let process_names = vec![ "vault server", "postgres", @@ -96,40 +89,23 @@ impl BootstrapManager { let _ = Command::new("pkill").args(["-9", "-f", name]).output(); } - - - let ports = vec![ - 8200, - 5432, - 9000, - 6379, - 8300, - 8081, - 8082, - 25, - 443, - 53, - ]; + let ports = vec![8200, 5432, 9000, 6379, 8300, 8081, 8082, 25, 443, 53]; for port in ports { - let _ = Command::new("fuser") .args(["-k", "-9", &format!("{}/tcp", port)]) .output(); } - std::thread::sleep(std::time::Duration::from_millis(1000)); info!("Stack processes terminated"); } - pub fn check_single_instance() -> Result { let stack_path = std::env::var("BOTSERVER_STACK_PATH") .unwrap_or_else(|_| "./botserver-stack".to_string()); let lock_file = PathBuf::from(&stack_path).join(".lock"); if lock_file.exists() { - if let Ok(pid_str) = fs::read_to_string(&lock_file) { if let Ok(pid) = pid_str.trim().parse::() { let check = Command::new("kill").args(["-0", &pid.to_string()]).output(); @@ -151,7 +127,6 @@ impl BootstrapManager { Ok(true) } - pub fn release_instance_lock() { let stack_path = std::env::var("BOTSERVER_STACK_PATH") .unwrap_or_else(|_| "./botserver-stack".to_string()); @@ -161,8 +136,6 @@ impl BootstrapManager { } } - - fn has_installed_stack() -> bool { let stack_path = std::env::var("BOTSERVER_STACK_PATH") .unwrap_or_else(|_| "./botserver-stack".to_string()); @@ -171,7 +144,6 @@ impl BootstrapManager { return false; } - let indicators = vec![ stack_dir.join("bin/vault/vault"), stack_dir.join("data/vault"), @@ -181,11 +153,7 @@ impl BootstrapManager { indicators.iter().any(|path| path.exists()) } - - - fn reset_vault_only() -> Result<()> { - if Self::has_installed_stack() { error!("REFUSING to reset Vault credentials - botserver-stack is installed!"); error!("If you need to re-initialize, manually delete botserver-stack directory first"); @@ -199,7 +167,6 @@ impl BootstrapManager { let vault_init = PathBuf::from(&stack_path).join("conf/vault/init.json"); let env_file = PathBuf::from("./.env"); - if vault_init.exists() { info!("Removing vault init.json for re-initialization..."); fs::remove_file(&vault_init)?; @@ -215,9 +182,7 @@ impl BootstrapManager { pub async fn start_all(&mut self) -> Result<()> { let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; - if pm.is_installed("vault") { - let vault_already_running = Command::new("sh") .arg("-c") .arg("curl -f -s 'http://localhost:8200/v1/sys/health?standbyok=true&uninitcode=200&sealedcode=200' >/dev/null 2>&1") @@ -240,7 +205,6 @@ impl BootstrapManager { } } - for i in 0..10 { let vault_ready = Command::new("sh") .arg("-c") @@ -261,41 +225,33 @@ impl BootstrapManager { } } - if let Err(e) = self.ensure_vault_unsealed().await { warn!("Vault unseal failed: {}", e); - - if Self::has_installed_stack() { error!("Vault failed to unseal but stack is installed - NOT re-initializing"); error!("Try manually restarting Vault or check ./botserver-stack/logs/vault/vault.log"); - let _ = Command::new("pkill") .args(["-9", "-f", "botserver-stack/bin/vault"]) .output(); tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - if let Err(e) = pm.start("vault") { warn!("Failed to restart Vault: {}", e); } tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - if let Err(e) = self.ensure_vault_unsealed().await { return Err(anyhow::anyhow!( "Vault failed to start/unseal after restart: {}. Manual intervention required.", e )); } } else { - warn!("No installed stack detected - proceeding with re-initialization"); - let _ = Command::new("pkill") .args(["-9", "-f", "botserver-stack/bin/vault"]) .output(); @@ -305,16 +261,13 @@ impl BootstrapManager { return Err(e); } - self.bootstrap().await?; - info!("Vault re-initialization complete"); return Ok(()); } } - info!("Initializing SecretsManager..."); match init_secrets_manager().await { Ok(_) => info!("SecretsManager initialized successfully"), @@ -328,12 +281,10 @@ impl BootstrapManager { } } - if pm.is_installed("tables") { info!("Starting PostgreSQL database..."); match pm.start("tables") { Ok(_child) => { - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; info!("PostgreSQL started"); } @@ -343,7 +294,6 @@ impl BootstrapManager { } } - let other_components = vec![ ComponentInfo { name: "cache" }, ComponentInfo { name: "drive" }, @@ -390,19 +340,14 @@ impl BootstrapManager { }) .collect(); - format!("{}!1Aa", base) } - - - pub async fn ensure_services_running(&mut self) -> Result<()> { info!("Ensuring critical services are running..."); let installer = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; - let vault_installed = installer.is_installed("vault"); let vault_initialized = self.stack_dir("conf/vault/init.json").exists(); @@ -411,10 +356,8 @@ impl BootstrapManager { Self::kill_stack_processes(); - self.bootstrap().await?; - info!("Bootstrap complete, verifying Vault is ready..."); tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; @@ -422,14 +365,10 @@ impl BootstrapManager { warn!("Failed to unseal Vault after bootstrap: {}", e); } - return Ok(()); } - - if installer.is_installed("vault") { - let vault_running = Command::new("sh") .arg("-c") .arg("curl -f -s 'http://localhost:8200/v1/sys/health?standbyok=true&uninitcode=200&sealedcode=200' >/dev/null 2>&1") @@ -455,12 +394,9 @@ impl BootstrapManager { info!("Vault is already running"); } - - if let Err(e) = self.ensure_vault_unsealed().await { let err_msg = e.to_string(); - if err_msg.contains("not running") || err_msg.contains("connection refused") { info!("Vault not running - starting it now..."); let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; @@ -471,14 +407,12 @@ impl BootstrapManager { } else { warn!("Vault unseal failed: {} - attempting Vault restart only", e); - let _ = Command::new("pkill") .args(["-9", "-f", "botserver-stack/bin/vault"]) .output(); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; if let Err(e) = pm.start("vault") { warn!("Failed to restart Vault: {}", e); @@ -487,12 +421,9 @@ impl BootstrapManager { tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; } - if let Err(e) = self.ensure_vault_unsealed().await { warn!("Vault still not responding after restart: {}", e); - - if Self::has_installed_stack() { error!("CRITICAL: Vault failed but botserver-stack is installed!"); error!("REFUSING to delete init.json or .env - this would destroy your installation"); @@ -503,14 +434,12 @@ impl BootstrapManager { )); } - warn!("No installed stack detected - attempting Vault re-initialization"); if let Err(reset_err) = Self::reset_vault_only() { error!("Failed to reset Vault: {}", reset_err); return Err(reset_err); } - info!("Re-initializing Vault only (preserving other services)..."); let pm_reinit = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?; @@ -531,7 +460,6 @@ impl BootstrapManager { info!("Vault recovery complete"); } - info!("Initializing SecretsManager..."); match init_secrets_manager().await { Ok(_) => info!("SecretsManager initialized successfully"), @@ -544,14 +472,12 @@ impl BootstrapManager { } } } else { - warn!("Vault (secrets) component not installed - run bootstrap first"); return Err(anyhow::anyhow!( "Vault not installed. Run bootstrap command first." )); } - if installer.is_installed("tables") { info!("Starting PostgreSQL database service..."); match installer.start("tables") { @@ -571,7 +497,6 @@ impl BootstrapManager { warn!("PostgreSQL (tables) component not installed"); } - if installer.is_installed("drive") { info!("Starting MinIO drive service..."); match installer.start("drive") { @@ -591,8 +516,6 @@ impl BootstrapManager { Ok(()) } - - async fn ensure_vault_unsealed(&self) -> Result<()> { let vault_init_path = self.stack_dir("conf/vault/init.json"); let vault_addr = "http://localhost:8200"; @@ -603,7 +526,6 @@ impl BootstrapManager { )); } - let init_json = fs::read_to_string(&vault_init_path)?; let init_data: serde_json::Value = serde_json::from_str(&init_json)?; @@ -622,8 +544,6 @@ impl BootstrapManager { )); } - - let vault_bin = self.vault_bin(); let mut status_str = String::new(); let mut parsed_status: Option = None; @@ -651,7 +571,6 @@ impl BootstrapManager { status_str = String::from_utf8_lossy(&status_output.stdout).to_string(); let stderr_str = String::from_utf8_lossy(&status_output.stderr).to_string(); - if status_str.contains("connection refused") || stderr_str.contains("connection refused") { @@ -667,20 +586,16 @@ impl BootstrapManager { } } - if connection_refused { warn!("Vault is not running after retries (connection refused)"); return Err(anyhow::anyhow!("Vault not running - needs to be started")); } - if let Some(status) = parsed_status { let initialized = status["initialized"].as_bool().unwrap_or(false); let sealed = status["sealed"].as_bool().unwrap_or(true); if !initialized { - - warn!("Vault is running but not initialized - data may have been deleted"); return Err(anyhow::anyhow!( "Vault not initialized - needs re-bootstrap" @@ -704,7 +619,6 @@ impl BootstrapManager { warn!("Vault unseal may have failed: {}", stderr); } - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; let verify_output = std::process::Command::new("sh") .arg("-c") @@ -727,8 +641,6 @@ impl BootstrapManager { info!("Vault unsealed successfully"); } } else { - - let vault_pid = std::process::Command::new("pgrep") .args(["-f", "vault server"]) .output() @@ -752,12 +664,10 @@ impl BootstrapManager { return Err(anyhow::anyhow!("Vault not responding properly")); } - std::env::set_var("VAULT_ADDR", vault_addr); std::env::set_var("VAULT_TOKEN", &root_token); std::env::set_var("VAULT_SKIP_VERIFY", "true"); - std::env::set_var( "VAULT_CACERT", self.stack_dir("conf/system/certificates/ca/ca.crt") @@ -784,51 +694,33 @@ impl BootstrapManager { pub async fn bootstrap(&mut self) -> Result<()> { info!("=== BOOTSTRAP STARTING ==="); - - info!("Cleaning up any existing stack processes..."); Self::kill_stack_processes(); - info!("Generating TLS certificates..."); if let Err(e) = self.generate_certificates().await { error!("Failed to generate certificates: {}", e); } - info!("Creating Vault configuration..."); if let Err(e) = self.create_vault_config().await { error!("Failed to create Vault config: {}", e); } - - let db_password = self.generate_secure_password(24); let drive_accesskey = self.generate_secure_password(20); let drive_secret = self.generate_secure_password(40); let cache_password = self.generate_secure_password(24); - info!("Configuring services through Vault..."); let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone()).unwrap(); - - - let required_components = vec![ - "vault", - "tables", - "directory", - "drive", - "cache", - "llm", - ]; - + let required_components = vec!["vault", "tables", "directory", "drive", "cache", "llm"]; let vault_needs_setup = !self.stack_dir("conf/vault/init.json").exists(); for component in required_components { - let is_installed = pm.is_installed(component); let needs_install = if component == "vault" { !is_installed || vault_needs_setup @@ -851,8 +743,6 @@ impl BootstrapManager { .and_then(|cfg| cfg.binary_name.clone()) .unwrap_or_else(|| component.to_string()); - - if component == "vault" || component == "tables" || component == "directory" { let _ = Command::new("sh") .arg("-c") @@ -875,7 +765,6 @@ impl BootstrapManager { } info!("Component {} installed successfully", component); - if component == "tables" { info!("Starting PostgreSQL database..."); match pm.start("tables") { @@ -889,7 +778,6 @@ impl BootstrapManager { } } - info!("Running database migrations..."); let database_url = format!("postgres://gbuser:{}@localhost:5432/botserver", db_password); @@ -912,7 +800,6 @@ impl BootstrapManager { } } - if component == "directory" { info!("Starting Directory (Zitadel) service..."); match pm.start("directory") { @@ -928,16 +815,13 @@ impl BootstrapManager { info!("Waiting for Directory to be ready..."); if let Err(e) = self.setup_directory().await { - warn!("Directory additional setup had issues: {}", e); } } - if component == "vault" { info!("Setting up Vault secrets service..."); - let vault_bin = self.stack_dir("bin/vault/vault"); if !vault_bin.exists() { error!("Vault binary not found at {:?}", vault_bin); @@ -945,7 +829,6 @@ impl BootstrapManager { } info!("Vault binary verified at {:?}", vault_bin); - let vault_log_path = self.stack_dir("logs/vault/vault.log"); if let Some(parent) = vault_log_path.parent() { if let Err(e) = fs::create_dir_all(parent) { @@ -953,7 +836,6 @@ impl BootstrapManager { } } - let vault_data_path = self.stack_dir("data/vault"); if let Err(e) = fs::create_dir_all(&vault_data_path) { error!("Failed to create vault data directory: {}", e); @@ -961,7 +843,6 @@ impl BootstrapManager { info!("Starting Vault server..."); - let vault_bin_dir = self.stack_dir("bin/vault"); let vault_start_cmd = format!( "cd {} && nohup ./vault server -config=../../conf/vault/config.hcl > ../../logs/vault/vault.log 2>&1 &", @@ -973,7 +854,6 @@ impl BootstrapManager { .status(); std::thread::sleep(std::time::Duration::from_secs(2)); - let check = std::process::Command::new("pgrep") .args(["-f", "vault server"]) .output(); @@ -1000,7 +880,6 @@ impl BootstrapManager { } } - let final_check = std::process::Command::new("pgrep") .args(["-f", "vault server"]) .output(); @@ -1038,7 +917,6 @@ impl BootstrapManager { return Err(anyhow::anyhow!("Vault setup failed: {}. Check ./botserver-stack/logs/vault/vault.log for details.", e)); } - info!("Initializing SecretsManager..."); debug!( "VAULT_ADDR={:?}, VAULT_TOKEN set={}", @@ -1084,9 +962,6 @@ impl BootstrapManager { Ok(()) } - - - async fn configure_services_in_directory(&self, db_password: &str) -> Result<()> { info!("Creating Zitadel configuration files..."); @@ -1101,11 +976,8 @@ impl BootstrapManager { fs::create_dir_all(zitadel_config_path.parent().unwrap())?; - let zitadel_db_password = self.generate_secure_password(24); - - let zitadel_config = format!( r#"Log: Level: info @@ -1146,15 +1018,12 @@ DefaultInstance: RefreshTokenIdleExpiration: 720h RefreshTokenExpiration: 2160h "#, - zitadel_db_password, - db_password, + zitadel_db_password, db_password, ); fs::write(&zitadel_config_path, zitadel_config)?; info!("Created zitadel.yaml configuration"); - - let steps_config = format!( r#"FirstInstance: InstanceName: "BotServer" @@ -1185,7 +1054,6 @@ DefaultInstance: fs::write(&steps_config_path, steps_config)?; info!("Created steps.yaml for first instance setup"); - info!("Creating zitadel database..."); let create_db_result = std::process::Command::new("sh") .arg("-c") @@ -1202,7 +1070,6 @@ DefaultInstance: } } - let create_user_result = std::process::Command::new("sh") .arg("-c") .arg(format!( @@ -1223,7 +1090,6 @@ DefaultInstance: Ok(()) } - async fn setup_caddy_proxy(&self) -> Result<()> { let caddy_config = self.stack_dir("conf/proxy/Caddyfile"); fs::create_dir_all(caddy_config.parent().unwrap())?; @@ -1276,14 +1142,12 @@ meet.botserver.local {{ Ok(()) } - async fn setup_coredns(&self) -> Result<()> { let dns_config = self.stack_dir("conf/dns/Corefile"); fs::create_dir_all(dns_config.parent().unwrap())?; let zone_file = self.stack_dir("conf/dns/botserver.local.zone"); - let corefile = r#"botserver.local:53 { file /botserver-stack/conf/dns/botserver.local.zone reload 10s @@ -1299,7 +1163,6 @@ meet.botserver.local {{ fs::write(dns_config, corefile)?; - let zone = r#"$ORIGIN botserver.local. $TTL 60 @ IN SOA ns1.botserver.local. admin.botserver.local. ( @@ -1336,21 +1199,17 @@ meet IN A 127.0.0.1 Ok(()) } - async fn setup_directory(&self) -> Result<()> { let config_path = PathBuf::from("./config/directory_config.json"); let pat_path = self.stack_dir("conf/directory/admin-pat.txt"); - tokio::fs::create_dir_all("./config").await?; - info!("Waiting for Zitadel to be ready..."); let mut attempts = 0; let max_attempts = 60; while attempts < max_attempts { - let health_check = std::process::Command::new("curl") .args(["-f", "-s", "http://localhost:8300/healthz"]) .output(); @@ -1370,10 +1229,8 @@ meet IN A 127.0.0.1 warn!("Zitadel health check timed out, continuing anyway..."); } - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - let admin_token = if pat_path.exists() { let token = fs::read_to_string(&pat_path)?; let token = token.trim().to_string(); @@ -1385,25 +1242,18 @@ meet IN A 127.0.0.1 None }; - let mut setup = DirectorySetup::new( - "http://localhost:8300".to_string(), - config_path, - ); - + let mut setup = DirectorySetup::new("http://localhost:8300".to_string(), config_path); if let Some(token) = admin_token { setup.set_admin_token(token); } else { - info!("Directory setup skipped - no admin token available"); info!("First instance setup created initial admin user via steps.yaml"); return Ok(()); } - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - let org_name = "default"; match setup .create_organization(org_name, "Default Organization") @@ -1412,10 +1262,8 @@ meet IN A 127.0.0.1 Ok(org_id) => { info!("Created default organization: {}", org_name); - let user_password = self.generate_secure_password(16); - match setup .create_user( &org_id, @@ -1437,12 +1285,10 @@ meet IN A 127.0.0.1 } } - match setup.create_oauth_application(&org_id).await { Ok((project_id, client_id, client_secret)) => { info!("Created OAuth2 application in project: {}", project_id); - let admin_user = crate::package_manager::setup::DefaultUser { id: "admin".to_string(), username: "admin".to_string(), @@ -1483,7 +1329,6 @@ meet IN A 127.0.0.1 Ok(()) } - async fn setup_vault( &self, db_password: &str, @@ -1495,13 +1340,11 @@ meet IN A 127.0.0.1 let vault_init_path = vault_conf_path.join("init.json"); let env_file_path = PathBuf::from("./.env"); - info!("Waiting for Vault to be ready..."); let mut attempts = 0; let max_attempts = 30; while attempts < max_attempts { - let ps_check = std::process::Command::new("sh") .arg("-c") .arg("pgrep -f 'vault server' || echo 'NOT_RUNNING'") @@ -1534,12 +1377,11 @@ meet IN A 127.0.0.1 if output.status.success() { info!("Vault is responding"); break; - } else { + } - let stderr = String::from_utf8_lossy(&output.stderr); - if !stderr.is_empty() && attempts % 5 == 0 { - debug!("Vault health check attempt {}: {}", attempts + 1, stderr); - } + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.is_empty() && attempts % 5 == 0 { + debug!("Vault health check attempt {}: {}", attempts + 1, stderr); } } else if attempts % 5 == 0 { warn!("Vault health check curl failed (attempt {})", attempts + 1); @@ -1573,12 +1415,10 @@ meet IN A 127.0.0.1 )); } - let vault_addr = "http://localhost:8200"; std::env::set_var("VAULT_ADDR", vault_addr); std::env::set_var("VAULT_SKIP_VERIFY", "true"); - let (unseal_key, root_token) = if vault_init_path.exists() { info!("Reading Vault initialization from init.json..."); let init_json = fs::read_to_string(&vault_init_path)?; @@ -1595,7 +1435,6 @@ meet IN A 127.0.0.1 (unseal_key, root_token) } else { - let env_token = if env_file_path.exists() { if let Ok(env_content) = fs::read_to_string(&env_file_path) { env_content @@ -1609,7 +1448,6 @@ meet IN A 127.0.0.1 None }; - info!("Initializing Vault..."); let vault_bin = self.vault_bin(); @@ -1626,12 +1464,9 @@ meet IN A 127.0.0.1 if stderr.contains("already initialized") { warn!("Vault already initialized but init.json not found"); - - if let Some(_token) = env_token { info!("Found VAULT_TOKEN in .env, checking if Vault is unsealed..."); - let status_check = std::process::Command::new("sh") .arg("-c") .arg(format!( @@ -1647,7 +1482,6 @@ meet IN A 127.0.0.1 { let sealed = status["sealed"].as_bool().unwrap_or(true); if !sealed { - warn!("Vault is already unsealed - continuing with existing token"); warn!("NOTE: Unseal key is lost - Vault will need manual unseal after restart"); return Ok(()); @@ -1655,7 +1489,6 @@ meet IN A 127.0.0.1 } } - error!("Vault is sealed and unseal key is lost (init.json missing)"); error!("Options:"); error!(" 1. If you have a backup of init.json, restore it to ./botserver-stack/conf/vault/init.json"); @@ -1667,7 +1500,6 @@ meet IN A 127.0.0.1 )); } - error!("Vault already initialized but credentials are lost"); error!("Options:"); error!(" 1. If you have a backup of init.json, restore it to ./botserver-stack/conf/vault/init.json"); @@ -1699,7 +1531,6 @@ meet IN A 127.0.0.1 return Err(anyhow::anyhow!("Failed to get Vault root token")); } - info!("Unsealing Vault..."); let vault_bin = self.vault_bin(); @@ -1718,10 +1549,8 @@ meet IN A 127.0.0.1 } } - std::env::set_var("VAULT_TOKEN", &root_token); - info!("Writing .env file with Vault configuration..."); let env_content = format!( r#"# BotServer Environment Configuration @@ -1750,14 +1579,12 @@ VAULT_CACHE_TTL=300 fs::write(&env_file_path, &env_content)?; info!(" * Created .env file with Vault configuration"); - info!("Re-initializing SecretsManager with Vault credentials..."); match init_secrets_manager().await { Ok(_) => info!(" * SecretsManager now connected to Vault"), Err(e) => warn!("SecretsManager re-init warning: {}", e), } - info!("Enabling KV secrets engine..."); let _ = std::process::Command::new("sh") .arg("-c") @@ -1767,11 +1594,8 @@ VAULT_CACHE_TTL=300 )) .output(); - - info!("Storing secrets in Vault (only if not existing)..."); - let vault_bin_clone = vault_bin.clone(); let secret_exists = |path: &str| -> bool { let output = std::process::Command::new("sh") @@ -1784,7 +1608,6 @@ VAULT_CACHE_TTL=300 output.map(|o| o.status.success()).unwrap_or(false) }; - if !secret_exists("secret/gbo/tables") { let _ = std::process::Command::new("sh") .arg("-c") @@ -1798,7 +1621,6 @@ VAULT_CACHE_TTL=300 info!(" Database credentials already exist - preserving"); } - if !secret_exists("secret/gbo/drive") { let _ = std::process::Command::new("sh") .arg("-c") @@ -1812,7 +1634,6 @@ VAULT_CACHE_TTL=300 info!(" Drive credentials already exist - preserving"); } - if !secret_exists("secret/gbo/cache") { let _ = std::process::Command::new("sh") .arg("-c") @@ -1826,9 +1647,7 @@ VAULT_CACHE_TTL=300 info!(" Cache credentials already exist - preserving"); } - if !secret_exists("secret/gbo/directory") { - use rand::Rng; let masterkey: String = rand::rng() .sample_iter(&rand::distr::Alphanumeric) @@ -1847,7 +1666,6 @@ VAULT_CACHE_TTL=300 info!(" Directory credentials already exist - preserving"); } - if !secret_exists("secret/gbo/llm") { let _ = std::process::Command::new("sh") .arg("-c") @@ -1861,7 +1679,6 @@ VAULT_CACHE_TTL=300 info!(" LLM credentials already exist - preserving"); } - if !secret_exists("secret/gbo/email") { let _ = std::process::Command::new("sh") .arg("-c") @@ -1875,7 +1692,6 @@ VAULT_CACHE_TTL=300 info!(" Email credentials already exist - preserving"); } - if !secret_exists("secret/gbo/encryption") { let encryption_key = self.generate_secure_password(32); let _ = std::process::Command::new("sh") @@ -1897,7 +1713,6 @@ VAULT_CACHE_TTL=300 Ok(()) } - pub async fn setup_email(&self) -> Result<()> { let config_path = PathBuf::from("./config/email_config.json"); let directory_config_path = PathBuf::from("./config/directory_config.json"); @@ -1907,7 +1722,6 @@ VAULT_CACHE_TTL=300 config_path, ); - let directory_config = if directory_config_path.exists() { Some(directory_config_path) } else { @@ -1934,10 +1748,8 @@ VAULT_CACHE_TTL=300 format!("{}/", config.drive.server) }; - let (access_key, secret_key) = if config.drive.access_key.is_empty() || config.drive.secret_key.is_empty() { - match crate::shared::utils::get_secrets_manager().await { Some(manager) if manager.is_enabled() => { match manager.get_drive_credentials().await { @@ -1977,8 +1789,6 @@ VAULT_CACHE_TTL=300 aws_sdk_s3::Client::from_conf(s3_config) } - - pub fn sync_templates_to_database(&self) -> Result<()> { let mut conn = establish_pg_connection()?; self.create_bots_from_templates(&mut conn)?; @@ -1986,7 +1796,6 @@ VAULT_CACHE_TTL=300 } pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> { - let possible_paths = [ "../bottemplates", "bottemplates", @@ -2039,7 +1848,6 @@ VAULT_CACHE_TTL=300 use crate::shared::models::schema::bots; use diesel::prelude::*; - let possible_paths = [ "../bottemplates", "bottemplates", @@ -2066,7 +1874,6 @@ VAULT_CACHE_TTL=300 } }; - let default_bot: Option<(uuid::Uuid, String)> = bots::table .filter(bots::is_active.eq(true)) .select((bots::id, bots::name)) @@ -2086,7 +1893,6 @@ VAULT_CACHE_TTL=300 default_bot_name, default_bot_id ); - let default_template = templates_dir.join("default.gbai"); info!("Looking for default template at: {:?}", default_template); if default_template.exists() { @@ -2116,8 +1922,6 @@ VAULT_CACHE_TTL=300 Ok(()) } - - fn sync_config_csv_to_db( &self, conn: &mut diesel::PgConnection, @@ -2135,7 +1939,6 @@ VAULT_CACHE_TTL=300 ); for (line_num, line) in lines.iter().enumerate().skip(1) { - let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; @@ -2151,7 +1954,6 @@ VAULT_CACHE_TTL=300 continue; } - let new_id = uuid::Uuid::new_v4(); match diesel::sql_query( @@ -2232,7 +2034,6 @@ VAULT_CACHE_TTL=300 const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); - if let Err(e) = conn.run_pending_migrations(MIGRATIONS) { error!("Failed to apply migrations: {}", e); return Err(anyhow::anyhow!("Migration error: {}", e)); @@ -2241,16 +2042,12 @@ VAULT_CACHE_TTL=300 Ok(()) } - async fn create_vault_config(&self) -> Result<()> { let vault_conf_dir = self.stack_dir("conf/vault"); let config_path = vault_conf_dir.join("config.hcl"); fs::create_dir_all(&vault_conf_dir)?; - - - let config = r#"# Vault Configuration # Generated by BotServer bootstrap # Note: Paths are relative to botserver-stack/bin/vault/ (Vault's working directory) @@ -2288,7 +2085,6 @@ log_level = "info" fs::write(&config_path, config)?; - fs::create_dir_all(self.stack_dir("data/vault"))?; info!( @@ -2298,19 +2094,15 @@ log_level = "info" Ok(()) } - async fn generate_certificates(&self) -> Result<()> { let cert_dir = self.stack_dir("conf/system/certificates"); - fs::create_dir_all(&cert_dir)?; fs::create_dir_all(cert_dir.join("ca"))?; - let ca_cert_path = cert_dir.join("ca/ca.crt"); let ca_key_path = cert_dir.join("ca/ca.key"); - let mut ca_params = CertificateParams::default(); ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); @@ -2333,17 +2125,14 @@ log_level = "info" let key_pair = KeyPair::generate()?; let cert = ca_params.self_signed(&key_pair)?; - fs::write(&ca_cert_path, cert.pem())?; fs::write(&ca_key_path, key_pair.serialize_pem())?; key_pair }; - let ca_issuer = Issuer::from_params(&ca_params, &ca_key_pair); - let botserver_dir = cert_dir.join("botserver"); fs::create_dir_all(&botserver_dir)?; @@ -2363,7 +2152,6 @@ log_level = "info" client_dn.push(DnType::CommonName, "botserver-client"); client_params.distinguished_name = client_dn; - client_params .subject_alt_names .push(rcgen::SanType::DnsName("botserver".to_string().try_into()?)); @@ -2381,8 +2169,6 @@ log_level = "info" ); } - - let services = vec![ ( "vault", @@ -2456,14 +2242,12 @@ log_level = "info" let cert_path = service_dir.join("server.crt"); let key_path = service_dir.join("server.key"); - if cert_path.exists() && key_path.exists() { continue; } info!("Generating certificate for {}", service); - let mut params = CertificateParams::default(); params.not_before = time::OffsetDateTime::now_utc(); params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365); @@ -2474,7 +2258,6 @@ log_level = "info" dn.push(DnType::CommonName, &format!("{}.botserver.local", service)); params.distinguished_name = dn; - for san in sans { params .subject_alt_names @@ -2484,11 +2267,9 @@ log_level = "info" let key_pair = KeyPair::generate()?; let cert = params.signed_by(&key_pair, &ca_issuer)?; - fs::write(cert_path, cert.pem())?; fs::write(key_path, key_pair.serialize_pem())?; - fs::copy(&ca_cert_path, service_dir.join("ca.crt"))?; } diff --git a/src/core/package_manager/facade.rs b/src/core/package_manager/facade.rs index ce42d5c5d..7e086b220 100644 --- a/src/core/package_manager/facade.rs +++ b/src/core/package_manager/facade.rs @@ -71,7 +71,6 @@ impl PackageManager { .await?; } if !component.data_download_list.is_empty() { - let cache_base = self.base_path.parent().unwrap_or(&self.base_path); let cache = DownloadCache::new(cache_base).ok(); @@ -83,18 +82,15 @@ impl PackageManager { .join(&component.name) .join(&filename); - if output_path.exists() { info!("Data file already exists: {:?}", output_path); continue; } - if let Some(parent) = output_path.parent() { std::fs::create_dir_all(parent)?; } - if let Some(ref c) = cache { if let Some(cached_path) = c.get_cached_path(&filename) { info!("Using cached data file: {:?}", cached_path); @@ -103,7 +99,6 @@ impl PackageManager { } } - let download_target = if let Some(ref c) = cache { c.get_cache_path(&filename) } else { @@ -114,7 +109,6 @@ impl PackageManager { println!("Downloading {}", url); utils::download_file(url, download_target.to_str().unwrap()).await?; - if cache.is_some() && download_target != output_path { std::fs::copy(&download_target, &output_path)?; info!("Copied cached file to: {:?}", output_path); @@ -127,10 +121,8 @@ impl PackageManager { pub fn install_container(&self, component: &ComponentConfig) -> Result { let container_name = format!("{}-{}", self.tenant, component.name); - let _ = Command::new("lxd").args(&["init", "--auto"]).output(); - let images = [ "ubuntu:24.04", "ubuntu:22.04", @@ -157,14 +149,13 @@ impl PackageManager { info!("Successfully created container with image: {}", image); success = true; break; - } else { - last_error = String::from_utf8_lossy(&output.stderr).to_string(); - warn!("Failed to create container with {}: {}", image, last_error); - - let _ = Command::new("lxc") - .args(&["delete", &container_name, "--force"]) - .output(); } + last_error = String::from_utf8_lossy(&output.stderr).to_string(); + warn!("Failed to create container with {}: {}", image, last_error); + + let _ = Command::new("lxc") + .args(&["delete", &container_name, "--force"]) + .output(); } if !success { @@ -176,7 +167,6 @@ impl PackageManager { std::thread::sleep(std::time::Duration::from_secs(15)); self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?; - self.exec_in_container( &container_name, "echo 'nameserver 8.8.8.8' > /etc/resolv.conf", @@ -186,7 +176,6 @@ impl PackageManager { "echo 'nameserver 8.8.4.4' >> /etc/resolv.conf", )?; - self.exec_in_container(&container_name, "apt-get update -qq")?; self.exec_in_container( &container_name, @@ -247,15 +236,12 @@ impl PackageManager { } self.setup_port_forwarding(&container_name, &component.ports)?; - let container_ip = self.get_container_ip(&container_name)?; - if component.name == "vault" { self.initialize_vault(&container_name, &container_ip)?; } - let (connection_info, env_vars) = self.generate_connection_info(&component.name, &container_ip, &component.ports); @@ -275,12 +261,9 @@ impl PackageManager { }) } - fn get_container_ip(&self, container_name: &str) -> Result { - std::thread::sleep(std::time::Duration::from_secs(2)); - let output = Command::new("lxc") .args(&["list", container_name, "-c", "4", "--format", "csv"]) .output()?; @@ -289,7 +272,6 @@ impl PackageManager { let ip_output = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !ip_output.is_empty() { - let ip = ip_output .split(|c| c == ' ' || c == '(') .next() @@ -301,7 +283,6 @@ impl PackageManager { } } - let output = Command::new("lxc") .args(&["exec", container_name, "--", "hostname", "-I"]) .output()?; @@ -318,15 +299,11 @@ impl PackageManager { Ok("unknown".to_string()) } - fn initialize_vault(&self, container_name: &str, ip: &str) -> Result<()> { info!("Initializing Vault..."); - std::thread::sleep(std::time::Duration::from_secs(5)); - - let output = Command::new("lxc") .args(&[ "exec", @@ -350,7 +327,6 @@ impl PackageManager { let init_output = String::from_utf8_lossy(&output.stdout); - let init_json: serde_json::Value = serde_json::from_str(&init_output).context("Failed to parse Vault init output")?; @@ -361,12 +337,10 @@ impl PackageManager { .as_str() .context("No root token in output")?; - let unseal_keys_file = PathBuf::from("vault-unseal-keys"); let mut unseal_content = String::new(); for (i, key) in unseal_keys.iter().enumerate() { if i < 3 { - unseal_content.push_str(&format!( "VAULT_UNSEAL_KEY_{}={}\n", i + 1, @@ -376,7 +350,6 @@ impl PackageManager { } std::fs::write(&unseal_keys_file, &unseal_content)?; - #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -385,7 +358,6 @@ impl PackageManager { info!("Created {}", unseal_keys_file.display()); - let env_file = PathBuf::from(".env"); let env_content = format!( "\n# Vault Configuration (auto-generated)\nVAULT_ADDR=http://{}:8200\nVAULT_TOKEN={}\nVAULT_UNSEAL_KEYS_FILE=vault-unseal-keys\n", @@ -393,11 +365,9 @@ impl PackageManager { ); if env_file.exists() { - let existing = std::fs::read_to_string(&env_file)?; if !existing.contains("VAULT_ADDR=") { - let mut file = std::fs::OpenOptions::new().append(true).open(&env_file)?; use std::io::Write; file.write_all(env_content.as_bytes())?; @@ -406,13 +376,10 @@ impl PackageManager { warn!(".env already contains VAULT_ADDR, not overwriting"); } } else { - std::fs::write(&env_file, env_content.trim_start())?; info!("Created .env with Vault config"); } - - for i in 0..3 { if let Some(key) = unseal_keys.get(i) { let key_str = key.as_str().unwrap_or(""); @@ -434,8 +401,6 @@ impl PackageManager { Ok(()) } - - fn generate_connection_info( &self, component: &str, @@ -445,9 +410,8 @@ impl PackageManager { let env_vars = HashMap::new(); let connection_info = match component { "vault" => { - format!( - r#"Vault Server: + r"Vault Server: URL: http://{}:8200 UI: http://{}:8200/ui @@ -466,141 +430,141 @@ Or manually: lxc exec {}-vault -- /opt/gbo/bin/vault operator unseal For other auto-unseal options (TPM, HSM, Transit), see: - https://generalbots.github.io/botbook/chapter-08/secrets-management.html"#, + https://generalbots.github.io/botbook/chapter-08/secrets-management.html", ip, ip, self.tenant ) } "vector_db" => { format!( - r#"Qdrant Vector Database: + r"Qdrant Vector Database: REST API: http://{}:6333 gRPC: {}:6334 Dashboard: http://{}:6333/dashboard Store credentials in Vault: - botserver vault put gbo/vectordb host={} port=6333"#, + botserver vault put gbo/vectordb host={} port=6333", ip, ip, ip, ip ) } "tables" => { format!( - r#"PostgreSQL Database: + r"PostgreSQL Database: Host: {} Port: 5432 Database: botserver User: gbuser Store credentials in Vault: - botserver vault put gbo/tables host={} port=5432 database=botserver username=gbuser password="#, + botserver vault put gbo/tables host={} port=5432 database=botserver username=gbuser password=", ip, ip ) } "drive" => { format!( - r#"MinIO Object Storage: + r"MinIO Object Storage: API: http://{}:9000 Console: http://{}:9001 Store credentials in Vault: - botserver vault put gbo/drive server={} port=9000 accesskey=minioadmin secret="#, + botserver vault put gbo/drive server={} port=9000 accesskey=minioadmin secret=", ip, ip, ip ) } "cache" => { format!( - r#"Redis/Valkey Cache: + r"Redis/Valkey Cache: Host: {} Port: 6379 Store credentials in Vault: - botserver vault put gbo/cache host={} port=6379 password="#, + botserver vault put gbo/cache host={} port=6379 password=", ip, ip ) } "email" => { format!( - r#"Email Server (Stalwart): + r"Email Server (Stalwart): SMTP: {}:25 IMAP: {}:143 Web: http://{}:8080 Store credentials in Vault: - botserver vault put gbo/email server={} port=25 username=admin password="#, + botserver vault put gbo/email server={} port=25 username=admin password=", ip, ip, ip, ip ) } "directory" => { format!( - r#"Zitadel Identity Provider: + r"Zitadel Identity Provider: URL: http://{}:8080 Console: http://{}:8080/ui/console Store credentials in Vault: - botserver vault put gbo/directory url=http://{}:8080 client_id= client_secret="#, + botserver vault put gbo/directory url=http://{}:8080 client_id= client_secret=", ip, ip, ip ) } "llm" => { format!( - r#"LLM Server (llama.cpp): + r"LLM Server (llama.cpp): API: http://{}:8081 Test: curl http://{}:8081/v1/models Store credentials in Vault: - botserver vault put gbo/llm url=http://{}:8081 local=true"#, + botserver vault put gbo/llm url=http://{}:8081 local=true", ip, ip, ip ) } "meeting" => { format!( - r#"LiveKit Meeting Server: + r"LiveKit Meeting Server: WebSocket: ws://{}:7880 API: http://{}:7880 Store credentials in Vault: - botserver vault put gbo/meet url=ws://{}:7880 api_key= api_secret="#, + botserver vault put gbo/meet url=ws://{}:7880 api_key= api_secret=", ip, ip, ip ) } "proxy" => { format!( - r#"Caddy Reverse Proxy: + r"Caddy Reverse Proxy: HTTP: http://{}:80 HTTPS: https://{}:443 - Admin: http://{}:2019"#, + Admin: http://{}:2019", ip, ip, ip ) } "timeseries_db" => { format!( - r#"InfluxDB Time Series Database: + r"InfluxDB Time Series Database: API: http://{}:8086 Store credentials in Vault: - botserver vault put gbo/observability url=http://{}:8086 token= org=pragmatismo bucket=metrics"#, + botserver vault put gbo/observability url=http://{}:8086 token= org=pragmatismo bucket=metrics", ip, ip ) } "observability" => { format!( - r#"Vector Log Aggregation: + r"Vector Log Aggregation: API: http://{}:8686 Store credentials in Vault: - botserver vault put gbo/observability vector_url=http://{}:8686"#, + botserver vault put gbo/observability vector_url=http://{}:8686", ip, ip ) } "alm" => { format!( - r#"Forgejo Git Server: + r"Forgejo Git Server: Web: http://{}:3000 SSH: {}:22 Store credentials in Vault: - botserver vault put gbo/alm url=http://{}:3000 token="#, + botserver vault put gbo/alm url=http://{}:3000 token=", ip, ip, ip ) } @@ -611,11 +575,11 @@ Store credentials in Vault: .collect::>() .join("\n"); format!( - r#"Component: {} + r"Component: {} Container: {}-{} IP: {} Ports: -{}"#, +{}", component, self.tenant, component, ip, ports_str ) } @@ -740,7 +704,6 @@ Store credentials in Vault: let bin_path = self.base_path.join("bin").join(component); std::fs::create_dir_all(&bin_path)?; - let cache_base = self.base_path.parent().unwrap_or(&self.base_path); let cache = DownloadCache::new(cache_base).unwrap_or_else(|e| { warn!("Failed to initialize download cache: {}", e); @@ -748,7 +711,6 @@ Store credentials in Vault: DownloadCache::new(&self.base_path).expect("Failed to create fallback cache") }); - let cache_result = cache.resolve_component_url(component, url); let source_file = match cache_result { @@ -763,7 +725,6 @@ Store credentials in Vault: info!("Downloading {} from {}", component, download_url); println!("Downloading {}", download_url); - self.download_with_reqwest(&download_url, &cache_path, component) .await?; @@ -772,7 +733,6 @@ Store credentials in Vault: } }; - self.handle_downloaded_file(&source_file, &bin_path, binary_name)?; Ok(()) } @@ -785,7 +745,6 @@ Store credentials in Vault: const MAX_RETRIES: u32 = 3; const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2); - if let Some(parent) = target_file.parent() { std::fs::create_dir_all(parent)?; } @@ -870,7 +829,6 @@ Store credentials in Vault: } else { let final_path = bin_path.join(temp_file.file_name().unwrap()); - if temp_file.to_string_lossy().contains("botserver-installers") { std::fs::copy(temp_file, &final_path)?; } else { @@ -894,7 +852,6 @@ Store credentials in Vault: )); } - if !temp_file.to_string_lossy().contains("botserver-installers") { std::fs::remove_file(temp_file)?; } @@ -912,7 +869,6 @@ Store credentials in Vault: )); } - #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -935,8 +891,6 @@ Store credentials in Vault: } } - - if !temp_file.to_string_lossy().contains("botserver-installers") { std::fs::remove_file(temp_file)?; } @@ -950,7 +904,6 @@ Store credentials in Vault: ) -> Result<()> { let final_path = bin_path.join(name); - if temp_file.to_string_lossy().contains("botserver-installers") { std::fs::copy(temp_file, &final_path)?; } else { @@ -981,7 +934,6 @@ Store credentials in Vault: PathBuf::from("/opt/gbo/data") }; - let conf_path = if target == "local" { self.base_path.join("conf") } else { @@ -993,15 +945,12 @@ Store credentials in Vault: PathBuf::from("/opt/gbo/logs") }; - let db_password = match get_database_url_sync() { Ok(url) => { let (_, password, _, _, _) = parse_database_url(&url); password } Err(_) => { - - trace!("Vault not available for DB_PASSWORD, using empty string"); String::new() } @@ -1124,8 +1073,6 @@ Store credentials in Vault: exec_cmd: &str, env_vars: &HashMap, ) -> Result<()> { - - let db_password = match get_database_url_sync() { Ok(url) => { let (_, password, _, _, _) = parse_database_url(&url); diff --git a/src/core/package_manager/setup/email_setup.rs b/src/core/package_manager/setup/email_setup.rs index 967b9f3cf..18093e6a8 100644 --- a/src/core/package_manager/setup/email_setup.rs +++ b/src/core/package_manager/setup/email_setup.rs @@ -5,7 +5,6 @@ use std::time::Duration; use tokio::fs; use tokio::time::sleep; - #[derive(Debug)] pub struct EmailSetup { base_url: String, @@ -34,7 +33,6 @@ pub struct EmailDomain { impl EmailSetup { pub fn new(base_url: String, config_path: PathBuf) -> Self { - let admin_user = format!( "admin_{}@botserver.local", uuid::Uuid::new_v4() @@ -53,7 +51,6 @@ impl EmailSetup { } } - fn generate_secure_password() -> String { use rand::distr::Alphanumeric; use rand::Rng; @@ -66,12 +63,10 @@ impl EmailSetup { .collect() } - pub async fn wait_for_ready(&self, max_attempts: u32) -> Result<()> { log::info!("Waiting for Email service to be ready..."); for attempt in 1..=max_attempts { - if let Ok(_) = tokio::net::TcpStream::connect("127.0.0.1:25").await { log::info!("Email service is ready!"); return Ok(()); @@ -88,27 +83,22 @@ impl EmailSetup { anyhow::bail!("Email service did not become ready in time") } - pub async fn initialize( &mut self, directory_config_path: Option, ) -> Result { log::info!(" Initializing Email (Stalwart) server..."); - if let Ok(existing_config) = self.load_existing_config().await { log::info!("Email already initialized, using existing config"); return Ok(existing_config); } - self.wait_for_ready(30).await?; - self.create_default_domain().await?; log::info!(" Created default email domain: localhost"); - let directory_integration = if let Some(dir_config_path) = directory_config_path { match self.setup_directory_integration(&dir_config_path).await { Ok(_) => { @@ -124,7 +114,6 @@ impl EmailSetup { false }; - self.create_admin_account().await?; log::info!(" Created admin email account: {}", self.admin_user); @@ -139,7 +128,6 @@ impl EmailSetup { directory_integration, }; - self.save_config(&config).await?; log::info!(" Saved Email configuration"); @@ -151,14 +139,10 @@ impl EmailSetup { Ok(config) } - async fn create_default_domain(&self) -> Result<()> { - - Ok(()) } - async fn create_admin_account(&self) -> Result<()> { log::info!("Creating admin email account via Stalwart API..."); @@ -166,14 +150,13 @@ impl EmailSetup { .timeout(Duration::from_secs(30)) .build()?; - let api_url = format!("{}/api/account", self.base_url); let account_data = serde_json::json!({ "name": self.admin_user, "secret": self.admin_pass, "description": "BotServer Admin Account", - "quota": 1073741824, + "quota": 1_073_741_824, "type": "individual", "emails": [self.admin_user.clone()], "memberOf": ["administrators"], @@ -196,7 +179,6 @@ impl EmailSetup { ); Ok(()) } else if resp.status().as_u16() == 409 { - log::info!("Admin email account already exists: {}", self.admin_user); Ok(()) } else { @@ -222,7 +204,6 @@ impl EmailSetup { } } - async fn setup_directory_integration(&self, directory_config_path: &PathBuf) -> Result<()> { let content = fs::read_to_string(directory_config_path).await?; let dir_config: serde_json::Value = serde_json::from_str(&content)?; @@ -234,31 +215,25 @@ impl EmailSetup { log::info!("Setting up OIDC authentication with Directory..."); log::info!("Issuer URL: {}", issuer_url); - - Ok(()) } - async fn save_config(&self, config: &EmailConfig) -> Result<()> { let json = serde_json::to_string_pretty(config)?; fs::write(&self.config_path, json).await?; Ok(()) } - async fn load_existing_config(&self) -> Result { let content = fs::read_to_string(&self.config_path).await?; let config: EmailConfig = serde_json::from_str(&content)?; Ok(config) } - pub async fn get_config(&self) -> Result { self.load_existing_config().await } - pub async fn create_user_mailbox( &self, _username: &str, @@ -267,20 +242,15 @@ impl EmailSetup { ) -> Result<()> { log::info!("Creating mailbox for user: {}", email); - - - Ok(()) } - pub async fn sync_users_from_directory(&self, directory_config_path: &PathBuf) -> Result<()> { log::info!("Syncing users from Directory to Email..."); let content = fs::read_to_string(directory_config_path).await?; let dir_config: serde_json::Value = serde_json::from_str(&content)?; - if let Some(default_user) = dir_config.get("default_user") { let email = default_user["email"].as_str().unwrap_or(""); let password = default_user["password"].as_str().unwrap_or(""); @@ -296,7 +266,6 @@ impl EmailSetup { } } - pub async fn generate_email_config( config_path: PathBuf, data_path: PathBuf, @@ -352,7 +321,6 @@ store = "sqlite" data_path.display() ); - if directory_integration { config.push_str( r#" diff --git a/src/drive/vectordb.rs b/src/drive/vectordb.rs index 2750e57f1..b5ed67d17 100644 --- a/src/drive/vectordb.rs +++ b/src/drive/vectordb.rs @@ -10,10 +10,7 @@ use uuid::Uuid; #[cfg(feature = "vectordb")] use qdrant_client::{ - qdrant::{ - vectors_config::Config, CreateCollection, Distance, PointStruct, VectorParams, - VectorsConfig, - }, + qdrant::{Distance, PointStruct, VectorParams}, Qdrant, }; diff --git a/src/email/mod.rs b/src/email/mod.rs index cd9b8f53d..7e4551bbb 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -21,9 +21,6 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; - - - #[derive(Debug, QueryableByName)] pub struct EmailAccountBasicRow { #[diesel(sql_type = DieselUuid)] @@ -36,7 +33,6 @@ pub struct EmailAccountBasicRow { pub is_primary: bool, } - #[derive(Debug, QueryableByName)] pub struct ImapCredentialsRow { #[diesel(sql_type = Text)] @@ -49,7 +45,6 @@ pub struct ImapCredentialsRow { pub password_encrypted: String, } - #[derive(Debug, QueryableByName)] pub struct SmtpCredentialsRow { #[diesel(sql_type = Text)] @@ -66,7 +61,6 @@ pub struct SmtpCredentialsRow { pub password_encrypted: String, } - #[derive(Debug, QueryableByName)] pub struct EmailSearchRow { #[diesel(sql_type = Text)] @@ -87,19 +81,12 @@ pub mod stalwart_client; pub mod stalwart_sync; pub mod vectordb; - -async fn extract_user_from_session(state: &Arc) -> Result { - - +async fn extract_user_from_session(_state: &Arc) -> Result { Ok(Uuid::new_v4()) } - - - pub fn configure() -> Router> { Router::new() - .route(ApiUrls::EMAIL_ACCOUNTS, get(list_email_accounts)) .route( &format!("{}/add", ApiUrls::EMAIL_ACCOUNTS), @@ -127,7 +114,6 @@ pub fn configure() -> Router> { .replace(":email", "{email}"), post(track_click), ) - .route( "/api/email/tracking/pixel/{tracking_id}", get(serve_tracking_pixel), @@ -138,7 +124,6 @@ pub fn configure() -> Router> { ) .route("/api/email/tracking/list", get(list_sent_emails_tracking)) .route("/api/email/tracking/stats", get(get_tracking_stats)) - .route("/ui/email/accounts", get(list_email_accounts_htmx)) .route("/ui/email/list", get(list_emails_htmx)) .route("/ui/email/folders", get(list_folders_htmx)) @@ -153,7 +138,6 @@ pub fn configure() -> Router> { .route("/ui/email/auto-responder", post(save_auto_responder)) } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SaveDraftRequest { pub account_id: String, @@ -164,9 +148,6 @@ pub struct SaveDraftRequest { pub body: String, } - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SentEmailTracking { pub id: String, @@ -187,7 +168,6 @@ pub struct SentEmailTracking { pub is_read: bool, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrackingStatusResponse { pub tracking_id: String, @@ -199,13 +179,11 @@ pub struct TrackingStatusResponse { pub read_count: i32, } - #[derive(Debug, Deserialize)] pub struct TrackingPixelQuery { pub t: Option, } - #[derive(Debug, Deserialize)] pub struct ListTrackingQuery { pub account_id: Option, @@ -214,7 +192,6 @@ pub struct ListTrackingQuery { pub filter: Option, } - #[derive(Debug, Serialize)] pub struct TrackingStatsResponse { pub total_sent: i64, @@ -223,8 +200,6 @@ pub struct TrackingStatsResponse { pub avg_time_to_read_hours: Option, } - - #[derive(Debug, Serialize, Deserialize)] pub struct EmailAccountRequest { pub email: String, @@ -332,9 +307,7 @@ pub struct ApiResponse { pub message: Option, } - - -struct EmailError(String); +pub struct EmailError(String); impl IntoResponse for EmailError { fn into_response(self) -> Response { @@ -344,12 +317,10 @@ impl IntoResponse for EmailError { impl From for EmailError { fn from(s: String) -> Self { - EmailError(s) + Self(s) } } - - fn parse_from_field(from: &str) -> (String, String) { if let Some(start) = from.find('<') { if let Some(end) = from.find('>') { @@ -362,7 +333,6 @@ fn parse_from_field(from: &str) -> (String, String) { } fn format_email_time(date_str: &str) -> String { - if date_str.is_empty() { return "Unknown".to_string(); } @@ -375,28 +345,26 @@ fn format_email_time(date_str: &str) -> String { } fn encrypt_password(password: &str) -> String { - - general_purpose::STANDARD.encode(password.as_bytes()) } fn decrypt_password(encrypted: &str) -> Result { - general_purpose::STANDARD .decode(encrypted) - .map_err(|e| format!("Decryption failed: {}", e)) + .map_err(|e| format!("Decryption failed: {e}")) .and_then(|bytes| { - String::from_utf8(bytes).map_err(|e| format!("UTF-8 conversion failed: {}", e)) + String::from_utf8(bytes).map_err(|e| format!("UTF-8 conversion failed: {e}")) }) } - - +/// Add a new email account. +/// +/// # Errors +/// Returns an error if authentication fails or database operations fail. pub async fn add_email_account( State(state): State>, Json(request): Json, ) -> Result>, EmailError> { - let current_user_id = match extract_user_from_session(&state).await { Ok(id) => id, Err(_) => return Err(EmailError("Authentication required".to_string())), @@ -405,7 +373,6 @@ pub async fn add_email_account( let account_id = Uuid::new_v4(); let encrypted_password = encrypt_password(&request.password); - let resp_email = request.email.clone(); let resp_display_name = request.display_name.clone(); let resp_imap_server = request.imap_server.clone(); @@ -416,8 +383,8 @@ pub async fn add_email_account( let conn = state.conn.clone(); tokio::task::spawn_blocking(move || { - use crate::shared::models::schema::user_email_accounts::dsl::*; - let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + use crate::shared::models::schema::user_email_accounts::dsl::{is_primary, user_email_accounts, user_id}; + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {e}"))?; if request.is_primary { @@ -437,20 +404,20 @@ pub async fn add_email_account( .bind::(&request.email) .bind::, _>(request.display_name.as_ref()) .bind::(&request.imap_server) - .bind::(request.imap_port as i32) + .bind::(i32::from(request.imap_port)) .bind::(&request.smtp_server) - .bind::(request.smtp_port as i32) + .bind::(i32::from(request.smtp_port)) .bind::(&request.username) .bind::(&encrypted_password) .bind::(request.is_primary) .bind::(true) .execute(&mut db_conn) - .map_err(|e| format!("Failed to insert account: {}", e))?; + .map_err(|e| format!("Failed to insert account: {e}"))?; Ok::<_, String>(account_id) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; Ok(Json(ApiResponse { @@ -471,9 +438,7 @@ pub async fn add_email_account( })) } - pub async fn list_email_accounts_htmx(State(state): State>) -> impl IntoResponse { - let user_id = match extract_user_from_session(&state).await { Ok(id) => id, Err(_) => { @@ -487,18 +452,18 @@ pub async fn list_email_accounts_htmx(State(state): State>) -> imp let conn = state.conn.clone(); let accounts = tokio::task::spawn_blocking(move || { - let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {e}"))?; diesel::sql_query( "SELECT id, email, display_name, is_primary FROM user_email_accounts WHERE user_id = $1 AND is_active = true ORDER BY is_primary DESC" ) .bind::(user_id) .load::(&mut db_conn) - .map_err(|e| format!("Query failed: {}", e)) + .map_err(|e| format!("Query failed: {e}")) }) .await .ok() - .and_then(|r| r.ok()) + .and_then(Result::ok) .unwrap_or_default(); if accounts.is_empty() { @@ -516,27 +481,31 @@ pub async fn list_email_accounts_htmx(State(state): State>) -> imp .clone() .unwrap_or_else(|| account.email.clone()); let primary_badge = if account.is_primary { - r##"Primary"## + r#"Primary"# } else { "" }; - html.push_str(&format!( - r##""#, account.id, name, primary_badge - )); + ); } axum::response::Html(html) } - +/// List all email accounts for the current user. +/// +/// # Errors +/// Returns an error if authentication fails or database operations fail. pub async fn list_email_accounts( State(state): State>, ) -> Result>>, EmailError> { - let current_user_id = match extract_user_from_session(&state).await { Ok(id) => id, Err(_) => return Err(EmailError("Authentication required".to_string())), @@ -544,10 +513,13 @@ pub async fn list_email_accounts( let conn = state.conn.clone(); let accounts = tokio::task::spawn_blocking(move || { - use crate::shared::models::schema::user_email_accounts::dsl::*; + use crate::shared::models::schema::user_email_accounts::dsl::{ + created_at, display_name, email, id, imap_port, imap_server, is_active, is_primary, + smtp_port, smtp_server, user_email_accounts, user_id, + }; let mut db_conn = conn .get() - .map_err(|e| format!("DB connection error: {}", e))?; + .map_err(|e| format!("DB connection error: {e}"))?; let results = user_email_accounts .filter(user_id.eq(current_user_id)) @@ -577,12 +549,12 @@ pub async fn list_email_accounts( bool, chrono::DateTime, )>(&mut db_conn) - .map_err(|e| format!("Query failed: {}", e))?; + .map_err(|e| format!("Query failed: {e}"))?; Ok::<_, String>(results) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; let account_list: Vec = accounts @@ -623,6 +595,10 @@ pub async fn list_email_accounts( })) } +/// Delete an email account. +/// +/// # Errors +/// Returns an error if the account ID is invalid or database operations fail. pub async fn delete_email_account( State(state): State>, Path(account_id): Path, @@ -634,17 +610,17 @@ pub async fn delete_email_account( tokio::task::spawn_blocking(move || { let mut db_conn = conn .get() - .map_err(|e| format!("DB connection error: {}", e))?; + .map_err(|e| format!("DB connection error: {e}"))?; diesel::sql_query("UPDATE user_email_accounts SET is_active = false WHERE id = $1") .bind::(account_uuid) .execute(&mut db_conn) - .map_err(|e| format!("Failed to delete account: {}", e))?; + .map_err(|e| format!("Failed to delete account: {e}"))?; Ok::<_, String>(()) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; Ok(Json(ApiResponse { @@ -654,8 +630,10 @@ pub async fn delete_email_account( })) } - - +/// List emails from a specific account and folder. +/// +/// # Errors +/// Returns an error if the account ID is invalid, IMAP connection fails, or emails cannot be fetched. pub async fn list_emails( State(state): State>, Json(request): Json, @@ -663,22 +641,21 @@ pub async fn list_emails( let account_uuid = Uuid::parse_str(&request.account_id) .map_err(|_| EmailError("Invalid account ID".to_string()))?; - let conn = state.conn.clone(); let account_info = tokio::task::spawn_blocking(move || { - let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {e}"))?; let result: ImapCredentialsRow = diesel::sql_query( "SELECT imap_server, imap_port, username, password_encrypted FROM user_email_accounts WHERE id = $1 AND is_active = true" ) .bind::(account_uuid) .get_result(&mut db_conn) - .map_err(|e| format!("Account not found: {}", e))?; + .map_err(|e| format!("Account not found: {e}"))?; Ok::<_, String>(result) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; let (imap_server, imap_port, username, encrypted_password) = ( @@ -689,29 +666,28 @@ pub async fn list_emails( ); let password = decrypt_password(&encrypted_password).map_err(EmailError)?; - let client = imap::ClientBuilder::new(imap_server.as_str(), imap_port as u16) .connect() - .map_err(|e| EmailError(format!("Failed to connect to IMAP: {:?}", e)))?; + .map_err(|e| EmailError(format!("Failed to connect to IMAP: {e:?}")))?; let mut session = client .login(&username, &password) - .map_err(|e| EmailError(format!("Login failed: {:?}", e)))?; + .map_err(|e| EmailError(format!("Login failed: {e:?}")))?; let folder = request.folder.unwrap_or_else(|| "INBOX".to_string()); session .select(&folder) - .map_err(|e| EmailError(format!("Failed to select folder: {:?}", e)))?; + .map_err(|e| EmailError(format!("Failed to select folder: {e:?}")))?; let messages = session .search("ALL") - .map_err(|e| EmailError(format!("Failed to search emails: {:?}", e)))?; + .map_err(|e| EmailError(format!("Failed to search emails: {e:?}")))?; let mut email_list = Vec::new(); let limit = request.limit.unwrap_or(50); let offset = request.offset.unwrap_or(0); - let recent_messages: Vec<_> = messages.iter().cloned().collect(); + let recent_messages: Vec<_> = messages.iter().copied().collect(); let recent_messages: Vec = recent_messages .into_iter() .rev() @@ -722,7 +698,7 @@ pub async fn list_emails( for seq in recent_messages { let fetch_result = session.fetch(seq.to_string(), "RFC822"); let messages = - fetch_result.map_err(|e| EmailError(format!("Failed to fetch email: {:?}", e)))?; + fetch_result.map_err(|e| EmailError(format!("Failed to fetch email: {e:?}")))?; for msg in messages.iter() { let body = msg @@ -730,7 +706,7 @@ pub async fn list_emails( .ok_or_else(|| EmailError("No body found".to_string()))?; let parsed = parse_mail(body) - .map_err(|e| EmailError(format!("Failed to parse email: {:?}", e)))?; + .map_err(|e| EmailError(format!("Failed to parse email: {e:?}")))?; let headers = parsed.get_headers(); let subject = headers.get_first_value("Subject").unwrap_or_default(); @@ -738,25 +714,22 @@ pub async fn list_emails( let to = headers.get_first_value("To").unwrap_or_default(); let date = headers.get_first_value("Date").unwrap_or_default(); - let body_text = if let Some(body_part) = parsed + let body_text = parsed .subparts .iter() .find(|p| p.ctype.mimetype == "text/plain") - { - body_part.get_body().unwrap_or_default() - } else { - parsed.get_body().unwrap_or_default() - }; + .map_or_else( + || parsed.get_body().unwrap_or_default(), + |body_part| body_part.get_body().unwrap_or_default(), + ); - let body_html = if let Some(body_part) = parsed + let body_html = parsed .subparts .iter() .find(|p| p.ctype.mimetype == "text/html") - { - body_part.get_body().unwrap_or_default() - } else { - String::new() - }; + .map_or_else(String::new, |body_part| { + body_part.get_body().unwrap_or_default() + }); let preview = body_text.lines().take(3).collect::>().join(" "); let preview_truncated = if preview.len() > 150 { @@ -800,6 +773,10 @@ pub async fn list_emails( })) } +/// Send an email from a specific account. +/// +/// # Errors +/// Returns an error if the account ID is invalid, SMTP connection fails, or email cannot be sent. pub async fn send_email( State(state): State>, Json(request): Json, @@ -807,12 +784,11 @@ pub async fn send_email( let account_uuid = Uuid::parse_str(&request.account_id) .map_err(|_| EmailError("Invalid account ID".to_string()))?; - let conn = state.conn.clone(); let account_info = tokio::task::spawn_blocking(move || { let mut db_conn = conn .get() - .map_err(|e| format!("DB connection error: {}", e))?; + .map_err(|e| format!("DB connection error: {e}"))?; let result: SmtpCredentialsRow = diesel::sql_query( "SELECT email, display_name, smtp_port, smtp_server, username, password_encrypted @@ -820,12 +796,12 @@ pub async fn send_email( ) .bind::(account_uuid) .get_result(&mut db_conn) - .map_err(|e| format!("Account not found: {}", e))?; + .map_err(|e| format!("Account not found: {e}"))?; Ok::<_, String>(result) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; let (from_email, display_name, smtp_port, smtp_server, username, encrypted_password) = ( @@ -841,62 +817,57 @@ pub async fn send_email( let from_addr = if display_name.is_empty() { from_email.clone() } else { - format!("{} <{}>", display_name, from_email) + format!("{display_name} <{from_email}>") }; - let pixel_enabled = is_tracking_pixel_enabled(&state, None).await; let tracking_id = Uuid::new_v4(); - let final_body = if pixel_enabled && request.is_html { inject_tracking_pixel(&request.body, &tracking_id.to_string(), &state).await } else { request.body.clone() }; - let mut email_builder = Message::builder() .from( from_addr .parse() - .map_err(|e| EmailError(format!("Invalid from address: {}", e)))?, + .map_err(|e| EmailError(format!("Invalid from address: {e}")))?, ) .to(request .to .parse() - .map_err(|e| EmailError(format!("Invalid to address: {}", e)))?) + .map_err(|e| EmailError(format!("Invalid to address: {e}")))?) .subject(request.subject.clone()); if let Some(ref cc) = request.cc { email_builder = email_builder.cc(cc .parse() - .map_err(|e| EmailError(format!("Invalid cc address: {}", e)))?); + .map_err(|e| EmailError(format!("Invalid cc address: {e}")))?); } if let Some(ref bcc) = request.bcc { email_builder = email_builder.bcc( bcc.parse() - .map_err(|e| EmailError(format!("Invalid bcc address: {}", e)))?, + .map_err(|e| EmailError(format!("Invalid bcc address: {e}")))?, ); } let email = email_builder .body(final_body) - .map_err(|e| EmailError(format!("Failed to build email: {}", e)))?; - + .map_err(|e| EmailError(format!("Failed to build email: {e}")))?; let creds = Credentials::new(username, password); let mailer = SmtpTransport::relay(&smtp_server) - .map_err(|e| EmailError(format!("Failed to create SMTP transport: {}", e)))? - .port(smtp_port as u16) + .map_err(|e| EmailError(format!("Failed to create SMTP transport: {e}")))? + .port(u16::try_from(smtp_port).unwrap_or(587)) .credentials(creds) .build(); mailer .send(&email) - .map_err(|e| EmailError(format!("Failed to send email: {}", e)))?; - + .map_err(|e| EmailError(format!("Failed to send email: {e}")))?; if pixel_enabled { let conn = state.conn.clone(); @@ -921,10 +892,7 @@ pub async fn send_email( .await; } - info!( - "Email sent successfully from account {} with tracking_id {}", - account_uuid, tracking_id - ); + info!("Email sent successfully from account {account_uuid} with tracking_id {tracking_id}"); Ok(Json(ApiResponse { success: true, @@ -933,6 +901,10 @@ pub async fn send_email( })) } +/// Save an email draft. +/// +/// # Errors +/// Returns an error if the account ID is invalid, authentication fails, or database operations fail. pub async fn save_draft( State(state): State>, Json(request): Json, @@ -940,7 +912,6 @@ pub async fn save_draft( let account_uuid = Uuid::parse_str(&request.account_id) .map_err(|_| EmailError("Invalid account ID".to_string()))?; - let user_id = match extract_user_from_session(&state).await { Ok(id) => id, Err(_) => return Err(EmailError("Authentication required".to_string())), @@ -949,7 +920,7 @@ pub async fn save_draft( let conn = state.conn.clone(); tokio::task::spawn_blocking(move || { - let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {e}"))?; diesel::sql_query( "INSERT INTO email_drafts (id, user_id, account_id, to_address, cc_address, bcc_address, subject, body) @@ -964,15 +935,13 @@ pub async fn save_draft( .bind::(&request.subject) .bind::(&request.body) .execute(&mut db_conn) - .map_err(|e| format!("Failed to save draft: {}", e))?; + .map_err(|e| format!("Failed to save draft: {e}"))?; Ok::<_, String>(()) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? - .map_err(|e| { - return EmailError(e); - })?; + .map_err(|e| EmailError(format!("Task join error: {e}")))? + .map_err(EmailError)?; Ok(Json(SaveDraftResponse { success: true, @@ -981,6 +950,10 @@ pub async fn save_draft( })) } +/// List all folders for an email account. +/// +/// # Errors +/// Returns an error if the account ID is invalid, IMAP connection fails, or folders cannot be listed. pub async fn list_folders( State(state): State>, Path(account_id): Path, @@ -988,22 +961,21 @@ pub async fn list_folders( let account_uuid = Uuid::parse_str(&account_id).map_err(|_| EmailError("Invalid account ID".to_string()))?; - let conn = state.conn.clone(); let account_info = tokio::task::spawn_blocking(move || { - let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {e}"))?; let result: ImapCredentialsRow = diesel::sql_query( "SELECT imap_server, imap_port, username, password_encrypted FROM user_email_accounts WHERE id = $1 AND is_active = true" ) .bind::(account_uuid) .get_result(&mut db_conn) - .map_err(|e| format!("Account not found: {}", e))?; + .map_err(|e| format!("Account not found: {e}"))?; Ok::<_, String>(result) }) .await - .map_err(|e| EmailError(format!("Task join error: {}", e)))? + .map_err(|e| EmailError(format!("Task join error: {e}")))? .map_err(EmailError)?; let (imap_server, imap_port, username, encrypted_password) = ( @@ -1014,19 +986,17 @@ pub async fn list_folders( ); let password = decrypt_password(&encrypted_password).map_err(EmailError)?; - - let client = imap::ClientBuilder::new(imap_server.as_str(), imap_port as u16) .connect() - .map_err(|e| format!("Failed to connect to IMAP: {:?}", e))?; + .map_err(|e| EmailError(format!("Failed to connect to IMAP: {e:?}")))?; let mut session = client .login(&username, &password) - .map_err(|e| EmailError(format!("Login failed: {:?}", e)))?; + .map_err(|e| EmailError(format!("Login failed: {e:?}")))?; let folders = session .list(None, Some("*")) - .map_err(|e| EmailError(format!("Failed to list folders: {:?}", e)))?; + .map_err(|e| EmailError(format!("Failed to list folders: {e:?}")))?; let folder_list: Vec = folders .iter() @@ -1047,9 +1017,11 @@ pub async fn list_folders( })) } - - -pub async fn get_latest_email_from( +/// Get the latest email from a specific sender. +/// +/// # Errors +/// Returns an error if the operation fails. +pub fn get_latest_email_from( State(_state): State>, Json(_request): Json, ) -> Result, EmailError> { @@ -1059,7 +1031,7 @@ pub async fn get_latest_email_from( }))) } -pub async fn save_click( +pub fn save_click( Path((campaign_id, email)): Path<(String, String)>, State(_state): State>, ) -> impl IntoResponse { @@ -1077,16 +1049,12 @@ pub async fn save_click( (StatusCode::OK, [("content-type", "image/gif")], pixel) } - - - const TRACKING_PIXEL: [u8; 43] = [ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B, ]; - async fn is_tracking_pixel_enabled(state: &Arc, bot_id: Option) -> bool { let config_manager = crate::core::config::ConfigManager::new(state.conn.clone()); let bot_id = bot_id.unwrap_or(Uuid::nil()); @@ -1097,13 +1065,11 @@ async fn is_tracking_pixel_enabled(state: &Arc, bot_id: Option) .unwrap_or(false) } - async fn inject_tracking_pixel( html_body: &str, tracking_id: &str, state: &Arc, ) -> String { - let config_manager = crate::core::config::ConfigManager::new(state.conn.clone()); let base_url = config_manager .get_config(&Uuid::nil(), "server-url", Some("http://localhost:8080")) @@ -1115,7 +1081,6 @@ async fn inject_tracking_pixel( pixel_url ); - if html_body.to_lowercase().contains("") { html_body .replace("", &format!("{}", pixel_html)) @@ -1125,7 +1090,6 @@ async fn inject_tracking_pixel( } } - fn save_email_tracking_record( conn: crate::shared::utils::DbPool, tracking_id: Uuid, @@ -1145,9 +1109,9 @@ fn save_email_tracking_record( let now = Utc::now(); diesel::sql_query( - r#"INSERT INTO sent_email_tracking + "INSERT INTO sent_email_tracking (id, tracking_id, bot_id, account_id, from_email, to_email, cc, bcc, subject, sent_at, read_count, is_read) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 0, false)"# + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 0, false)" ) .bind::(id) .bind::(tracking_id) @@ -1166,14 +1130,12 @@ fn save_email_tracking_record( Ok(()) } - pub async fn serve_tracking_pixel( Path(tracking_id): Path, State(state): State>, Query(_query): Query, headers: axum::http::HeaderMap, ) -> impl IntoResponse { - let client_ip = headers .get("x-forwarded-for") .and_then(|v| v.to_str().ok()) @@ -1190,13 +1152,11 @@ pub async fn serve_tracking_pixel( .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); - if let Ok(tracking_uuid) = Uuid::parse_str(&tracking_id) { let conn = state.conn.clone(); let ip_clone = client_ip.clone(); let ua_clone = user_agent.clone(); - let _ = tokio::task::spawn_blocking(move || { update_email_read_status(conn, tracking_uuid, ip_clone, ua_clone) }) @@ -1210,8 +1170,6 @@ pub async fn serve_tracking_pixel( warn!("Invalid tracking ID received: {}", tracking_id); } - - ( StatusCode::OK, [ @@ -1227,7 +1185,6 @@ pub async fn serve_tracking_pixel( ) } - fn update_email_read_status( conn: crate::shared::utils::DbPool, tracking_id: Uuid, @@ -1239,7 +1196,6 @@ fn update_email_read_status( .map_err(|e| format!("DB connection error: {}", e))?; let now = Utc::now(); - diesel::sql_query( r#"UPDATE sent_email_tracking SET @@ -1263,7 +1219,6 @@ fn update_email_read_status( Ok(()) } - pub async fn get_tracking_status( Path(tracking_id): Path, State(state): State>, @@ -1284,7 +1239,6 @@ pub async fn get_tracking_status( })) } - fn get_tracking_record( conn: crate::shared::utils::DbPool, tracking_id: Uuid, @@ -1330,7 +1284,6 @@ fn get_tracking_record( }) } - pub async fn list_sent_emails_tracking( State(state): State>, Query(query): Query, @@ -1348,7 +1301,6 @@ pub async fn list_sent_emails_tracking( })) } - fn list_tracking_records( conn: crate::shared::utils::DbPool, query: ListTrackingQuery, @@ -1378,22 +1330,21 @@ fn list_tracking_records( read_count: i32, } - let base_query = match query.filter.as_deref() { Some("read") => { - r#"SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count - FROM sent_email_tracking WHERE is_read = true - ORDER BY sent_at DESC LIMIT $1 OFFSET $2"# + "SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count + FROM sent_email_tracking WHERE account_id = $1 AND is_read = true + ORDER BY sent_at DESC LIMIT $2 OFFSET $3" } Some("unread") => { - r#"SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count - FROM sent_email_tracking WHERE is_read = false - ORDER BY sent_at DESC LIMIT $1 OFFSET $2"# + "SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count + FROM sent_email_tracking WHERE account_id = $1 AND is_read = false + ORDER BY sent_at DESC LIMIT $2 OFFSET $3" } _ => { - r#"SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count - FROM sent_email_tracking - ORDER BY sent_at DESC LIMIT $1 OFFSET $2"# + "SELECT tracking_id, to_email, subject, sent_at, is_read, read_at, read_count + FROM sent_email_tracking WHERE account_id = $1 + ORDER BY sent_at DESC LIMIT $2 OFFSET $3" } }; @@ -1417,7 +1368,6 @@ fn list_tracking_records( .collect()) } - pub async fn get_tracking_stats( State(state): State>, ) -> Result>, EmailError> { @@ -1434,7 +1384,6 @@ pub async fn get_tracking_stats( })) } - fn calculate_tracking_stats( conn: crate::shared::utils::DbPool, ) -> Result { @@ -1484,8 +1433,6 @@ pub async fn get_emails( "No emails tracked".to_string() } - - pub struct EmailService { state: Arc, } @@ -1542,18 +1489,14 @@ impl EmailService { to: &str, subject: &str, body: &str, - attachment: Vec, - filename: &str, + _attachment: Vec, + _filename: &str, ) -> Result<(), Box> { - - self.send_email(to, subject, body, None).await } } - pub async fn fetch_latest_sent_to(config: &EmailConfig, to: &str) -> Result { - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1566,7 +1509,6 @@ pub async fn fetch_latest_sent_to(config: &EmailConfig, to: &str) -> Result Result<(), String> { use chrono::Utc; - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1603,7 +1544,6 @@ pub async fn save_email_draft( .login(&config.username, &config.password) .map_err(|e| format!("Login failed: {:?}", e))?; - let date = Utc::now().to_rfc2822(); let message_id = format!("<{}.{}@botserver>", Uuid::new_v4(), config.server); let cc_header = if let Some(cc) = &draft.cc { @@ -1625,7 +1565,6 @@ pub async fn save_email_draft( date, config.from, draft.to, cc_header, draft.subject, message_id, draft.body ); - let folder = session .list(None, Some("Drafts")) .map_err(|e| format!("List folders failed: {}", e))? @@ -1644,13 +1583,10 @@ pub async fn save_email_draft( Ok(()) } - - async fn fetch_emails_from_folder( config: &EmailConfig, folder: &str, ) -> Result, String> { - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1708,7 +1644,6 @@ async fn get_folder_counts( ) -> Result, String> { use std::collections::HashMap; - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1730,7 +1665,6 @@ async fn get_folder_counts( } async fn fetch_email_by_id(config: &EmailConfig, id: &str) -> Result { - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1783,7 +1717,6 @@ async fn fetch_email_by_id(config: &EmailConfig, id: &str) -> Result Result<(), String> { - let client = imap::ClientBuilder::new(&config.server, config.port as u16) .connect() .map_err(|e| format!("Connection error: {}", e))?; @@ -1796,7 +1729,6 @@ async fn move_email_to_trash(config: &EmailConfig, id: &str) -> Result<(), Strin .select("INBOX") .map_err(|e| format!("Select failed: {}", e))?; - session .store(id, "+FLAGS (\\Deleted)") .map_err(|e| format!("Store failed: {}", e))?; @@ -1828,21 +1760,16 @@ struct EmailContent { body: String, } - - - pub async fn list_emails_htmx( State(state): State>, Query(params): Query>, ) -> Result { let folder = params.get("folder").unwrap_or(&"inbox".to_string()).clone(); - let user_id = extract_user_from_session(&state) .await .map_err(|_| EmailError("Authentication required".to_string()))?; - let conn = state.conn.clone(); let account = tokio::task::spawn_blocking(move || { let mut db_conn = conn @@ -1869,7 +1796,6 @@ pub async fn list_emails_htmx( )); }; - let config = EmailConfig { username: account.username.clone(), password: account.password.clone(), @@ -1885,7 +1811,7 @@ pub async fn list_emails_htmx( .unwrap_or_default(); let mut html = String::new(); - for (idx, email) in emails.iter().enumerate() { + for email in &emails { let unread_class = if email.unread { "unread" } else { "" }; html.push_str(&format!( r##"
>, ) -> Result { - let user_id = extract_user_from_session(&state) .await .map_err(|_| EmailError("Authentication required".to_string()))?; @@ -1949,7 +1873,6 @@ pub async fn list_folders_htmx( let account = account.unwrap(); - let config = EmailConfig { username: account.username.clone(), password: account.password.clone(), @@ -2008,9 +1931,8 @@ pub async fn list_folders_htmx( Ok(axum::response::Html(html)) } - pub async fn compose_email_htmx( - State(state): State>, + State(_state): State>, ) -> Result { let html = r##"
@@ -2044,12 +1966,10 @@ pub async fn compose_email_htmx( Ok(axum::response::Html(html)) } - pub async fn get_email_content_htmx( State(state): State>, Path(id): Path, ) -> Result { - let user_id = extract_user_from_session(&state) .await .map_err(|_| EmailError("Authentication required".to_string()))?; @@ -2079,7 +1999,6 @@ pub async fn get_email_content_htmx( )); }; - let config = EmailConfig { username: account.username.clone(), password: account.password.clone(), @@ -2135,12 +2054,10 @@ pub async fn get_email_content_htmx( Ok(axum::response::Html(html)) } - pub async fn delete_email_htmx( State(state): State>, Path(id): Path, ) -> Result { - let user_id = extract_user_from_session(&state) .await .map_err(|_| EmailError("Authentication required".to_string()))?; @@ -2172,7 +2089,6 @@ pub async fn delete_email_htmx( smtp_port: account.smtp_port as u16, }; - move_email_to_trash(&config, &id) .await .map_err(|e| EmailError(format!("Failed to delete email: {}", e)))?; @@ -2180,15 +2096,12 @@ pub async fn delete_email_htmx( info!("Email {} moved to trash", id); - list_emails_htmx(State(state), Query(std::collections::HashMap::new())).await } - pub async fn get_latest_email( - State(state): State>, + State(_state): State>, ) -> Result>, EmailError> { - Ok(Json(ApiResponse { success: true, data: Some(EmailData { @@ -2204,12 +2117,10 @@ pub async fn get_latest_email( })) } - pub async fn get_email( - State(state): State>, + State(_state): State>, Path(campaign_id): Path, ) -> Result>, EmailError> { - Ok(Json(ApiResponse { success: true, data: Some(EmailData { @@ -2225,7 +2136,6 @@ pub async fn get_email( })) } - pub async fn track_click( State(state): State>, Path((campaign_id, email)): Path<(String, String)>, @@ -2253,13 +2163,12 @@ pub struct EmailData { pub unread: bool, } - #[derive(Debug, QueryableByName)] struct EmailAccountRow { #[diesel(sql_type = diesel::sql_types::Uuid)] - pub id: Uuid, + pub _id: Uuid, #[diesel(sql_type = diesel::sql_types::Uuid)] - pub user_id: Uuid, + pub _user_id: Uuid, #[diesel(sql_type = diesel::sql_types::Text)] pub email: String, #[diesel(sql_type = diesel::sql_types::Text)] @@ -2276,11 +2185,7 @@ struct EmailAccountRow { pub smtp_port: i32, } - - - pub async fn list_labels_htmx(State(_state): State>) -> impl IntoResponse { - axum::response::Html( r#"
@@ -2304,7 +2209,6 @@ pub async fn list_labels_htmx(State(_state): State>) -> impl IntoR ) } - pub async fn list_templates_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#" @@ -2328,7 +2232,6 @@ pub async fn list_templates_htmx(State(_state): State>) -> impl In ) } - pub async fn list_signatures_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#" @@ -2348,7 +2251,6 @@ pub async fn list_signatures_htmx(State(_state): State>) -> impl I ) } - pub async fn list_rules_htmx(State(_state): State>) -> impl IntoResponse { axum::response::Html( r#" @@ -2380,7 +2282,6 @@ pub async fn list_rules_htmx(State(_state): State>) -> impl IntoRe ) } - pub async fn search_emails_htmx( State(state): State>, Query(params): Query>, @@ -2453,7 +2354,7 @@ pub async fn search_emails_htmx( let mut html = String::from(r##"
"##); html.push_str(&format!( - r##"
Found {} result(s) for "{}"
"##, + r#"
Found {} results for "{}"
"#, results.len(), query )); @@ -2482,14 +2383,12 @@ pub async fn search_emails_htmx( axum::response::Html(html) } - pub async fn save_auto_responder( State(_state): State>, axum::Form(form): axum::Form>, ) -> impl IntoResponse { info!("Saving auto-responder settings: {:?}", form); - axum::response::Html( r#"
diff --git a/src/email/stalwart_client.rs b/src/email/stalwart_client.rs index ae1d8484e..7fe38941d 100644 --- a/src/email/stalwart_client.rs +++ b/src/email/stalwart_client.rs @@ -1,23 +1,3 @@ - - - - - - - - - - - - - - - - - - - - use anyhow::{anyhow, Context, Result}; use chrono::{DateTime, NaiveDate, Utc}; use reqwest::{Client, Method, StatusCode}; @@ -26,23 +6,14 @@ use serde_json::{json, Value}; use std::time::Duration; use tracing::{debug, error, info, warn}; - - - const DEFAULT_TIMEOUT_SECS: u64 = 30; - pub const DEFAULT_QUEUE_POLL_INTERVAL_SECS: u64 = 30; - pub const DEFAULT_METRICS_POLL_INTERVAL_SECS: u64 = 60; - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueueStatus { - pub is_running: bool, pub total_queued: u64, @@ -50,10 +21,8 @@ pub struct QueueStatus { pub messages: Vec, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueuedMessage { - pub id: String, pub from: String, @@ -81,7 +50,6 @@ pub struct QueuedMessage { pub queued_at: Option>, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DeliveryStatus { @@ -94,7 +62,6 @@ pub enum DeliveryStatus { Unknown, } - #[derive(Debug, Clone, Deserialize)] struct QueueListResponse { #[serde(default)] @@ -103,9 +70,6 @@ struct QueueListResponse { items: Vec, } - - - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum PrincipalType { @@ -119,10 +83,8 @@ pub enum PrincipalType { Other, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Principal { - pub id: Option, #[serde(rename = "type")] @@ -149,10 +111,8 @@ pub struct Principal { pub disabled: bool, } - #[derive(Debug, Clone, Serialize)] pub struct AccountUpdate { - pub action: String, pub field: String, @@ -161,7 +121,6 @@ pub struct AccountUpdate { } impl AccountUpdate { - pub fn set(field: &str, value: impl Into) -> Self { Self { action: "set".to_string(), @@ -170,7 +129,6 @@ impl AccountUpdate { } } - pub fn add_item(field: &str, value: impl Into) -> Self { Self { action: "addItem".to_string(), @@ -179,7 +137,6 @@ impl AccountUpdate { } } - pub fn remove_item(field: &str, value: impl Into) -> Self { Self { action: "removeItem".to_string(), @@ -188,7 +145,6 @@ impl AccountUpdate { } } - pub fn clear(field: &str) -> Self { Self { action: "clear".to_string(), @@ -198,12 +154,8 @@ impl AccountUpdate { } } - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoResponderConfig { - pub enabled: bool, pub subject: String, @@ -235,7 +187,8 @@ impl Default for AutoResponderConfig { Self { enabled: false, subject: "Out of Office".to_string(), - body_plain: "I am currently out of the office and will respond upon my return.".to_string(), + body_plain: "I am currently out of the office and will respond upon my return." + .to_string(), body_html: None, start_date: None, end_date: None, @@ -245,10 +198,8 @@ impl Default for AutoResponderConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailRule { - pub id: String, pub name: String, @@ -270,10 +221,8 @@ fn default_stop_processing() -> bool { true } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleCondition { - pub field: String, pub operator: String, @@ -287,22 +236,16 @@ pub struct RuleCondition { pub case_sensitive: bool, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleAction { - pub action_type: String, #[serde(default)] pub value: String, } - - - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Metrics { - #[serde(default)] pub messages_received: u64, @@ -334,10 +277,8 @@ pub struct Metrics { pub extra: std::collections::HashMap, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { - pub timestamp: DateTime, pub level: String, @@ -351,7 +292,6 @@ pub struct LogEntry { pub context: Option, } - #[derive(Debug, Clone, Deserialize)] pub struct LogList { #[serde(default)] @@ -360,10 +300,8 @@ pub struct LogList { pub items: Vec, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TraceEvent { - pub timestamp: DateTime, pub event_type: String, @@ -390,7 +328,6 @@ pub struct TraceEvent { pub duration_ms: Option, } - #[derive(Debug, Clone, Deserialize)] pub struct TraceList { #[serde(default)] @@ -399,12 +336,8 @@ pub struct TraceList { pub items: Vec, } - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Report { - pub id: String, pub report_type: String, @@ -423,7 +356,6 @@ pub struct Report { pub data: Value, } - #[derive(Debug, Clone, Deserialize)] pub struct ReportList { #[serde(default)] @@ -432,12 +364,8 @@ pub struct ReportList { pub items: Vec, } - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpamClassifyRequest { - pub from: String, pub to: Vec, @@ -455,10 +383,8 @@ pub struct SpamClassifyRequest { pub body: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpamClassifyResult { - pub score: f64, pub classification: String, @@ -470,10 +396,8 @@ pub struct SpamClassifyResult { pub action: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpamTest { - pub name: String, pub score: f64, @@ -482,9 +406,6 @@ pub struct SpamTest { pub description: Option, } - - - #[derive(Debug, Deserialize)] #[serde(untagged)] enum ApiResponse { @@ -493,9 +414,6 @@ enum ApiResponse { Error { error: String }, } - - - #[derive(Debug, Clone)] pub struct StalwartClient { base_url: String, @@ -504,18 +422,6 @@ pub struct StalwartClient { } impl StalwartClient { - - - - - - - - - - - - pub fn new(base_url: &str, token: &str) -> Self { let http_client = Client::builder() .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) @@ -529,7 +435,6 @@ impl StalwartClient { } } - pub fn with_timeout(base_url: &str, token: &str, timeout_secs: u64) -> Self { let http_client = Client::builder() .timeout(Duration::from_secs(timeout_secs)) @@ -543,7 +448,6 @@ impl StalwartClient { } } - async fn request( &self, method: Method, @@ -563,20 +467,24 @@ impl StalwartClient { req = req.header("Content-Type", "application/json").json(b); } - let resp = req.send().await.context("Failed to send request to Stalwart")?; + let resp = req + .send() + .await + .context("Failed to send request to Stalwart")?; let status = resp.status(); if !status.is_success() { - let error_text = resp.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + let error_text = resp + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); error!("Stalwart API error: {} - {}", status, error_text); return Err(anyhow!("Stalwart API error ({}): {}", status, error_text)); } let text = resp.text().await.context("Failed to read response body")?; - if text.is_empty() || text == "null" { - return serde_json::from_str("null") .or_else(|_| serde_json::from_str("{}")) .or_else(|_| serde_json::from_str("true")) @@ -586,8 +494,13 @@ impl StalwartClient { serde_json::from_str(&text).context("Failed to parse Stalwart API response") } - - async fn request_raw(&self, method: Method, path: &str, body: &str, content_type: &str) -> Result<()> { + async fn request_raw( + &self, + method: Method, + path: &str, + body: &str, + content_type: &str, + ) -> Result<()> { let url = format!("{}{}", self.base_url, path); debug!("Stalwart API raw request: {} {}", method, url); @@ -603,18 +516,16 @@ impl StalwartClient { let status = resp.status(); if !status.is_success() { - let error_text = resp.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + let error_text = resp + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); return Err(anyhow!("Stalwart API error ({}): {}", status, error_text)); } Ok(()) } - - - - - pub async fn get_queue_status(&self) -> Result { let status: bool = self .request(Method::GET, "/api/queue/status", None) @@ -636,7 +547,6 @@ impl StalwartClient { }) } - pub async fn get_queued_message(&self, message_id: &str) -> Result { self.request( Method::GET, @@ -646,7 +556,6 @@ impl StalwartClient { .await } - pub async fn list_queued_messages( &self, limit: u32, @@ -660,7 +569,6 @@ impl StalwartClient { self.request(Method::GET, &path, None).await } - pub async fn retry_delivery(&self, message_id: &str) -> Result { self.request( Method::PATCH, @@ -670,7 +578,6 @@ impl StalwartClient { .await } - pub async fn cancel_delivery(&self, message_id: &str) -> Result { self.request( Method::DELETE, @@ -680,17 +587,16 @@ impl StalwartClient { .await } - pub async fn stop_queue(&self) -> Result { - self.request(Method::PATCH, "/api/queue/status/stop", None).await + self.request(Method::PATCH, "/api/queue/status/stop", None) + .await } - pub async fn start_queue(&self) -> Result { - self.request(Method::PATCH, "/api/queue/status/start", None).await + self.request(Method::PATCH, "/api/queue/status/start", None) + .await } - pub async fn get_failed_delivery_count(&self) -> Result { let resp: QueueListResponse = self .request( @@ -702,11 +608,6 @@ impl StalwartClient { Ok(resp.total) } - - - - - pub async fn create_account( &self, email: &str, @@ -725,19 +626,19 @@ impl StalwartClient { "roles": ["user"] }); - self.request(Method::POST, "/api/principal", Some(body)).await + self.request(Method::POST, "/api/principal", Some(body)) + .await } - pub async fn create_account_full(&self, principal: &Principal, password: &str) -> Result { let mut body = serde_json::to_value(principal)?; if let Some(obj) = body.as_object_mut() { obj.insert("secrets".to_string(), json!([password])); } - self.request(Method::POST, "/api/principal", Some(body)).await + self.request(Method::POST, "/api/principal", Some(body)) + .await } - pub async fn create_distribution_list( &self, name: &str, @@ -752,10 +653,10 @@ impl StalwartClient { "description": format!("Distribution list: {}", name) }); - self.request(Method::POST, "/api/principal", Some(body)).await + self.request(Method::POST, "/api/principal", Some(body)) + .await } - pub async fn create_shared_mailbox( &self, name: &str, @@ -770,20 +671,15 @@ impl StalwartClient { "description": format!("Shared mailbox: {}", name) }); - self.request(Method::POST, "/api/principal", Some(body)).await + self.request(Method::POST, "/api/principal", Some(body)) + .await } - pub async fn get_account(&self, account_id: &str) -> Result { - self.request( - Method::GET, - &format!("/api/principal/{}", account_id), - None, - ) - .await + self.request(Method::GET, &format!("/api/principal/{}", account_id), None) + .await } - pub async fn get_account_by_email(&self, email: &str) -> Result { self.request( Method::GET, @@ -793,8 +689,11 @@ impl StalwartClient { .await } - - pub async fn update_account(&self, account_id: &str, updates: Vec) -> Result<()> { + pub async fn update_account( + &self, + account_id: &str, + updates: Vec, + ) -> Result<()> { let body: Vec = updates .iter() .map(|u| { @@ -815,7 +714,6 @@ impl StalwartClient { Ok(()) } - pub async fn delete_account(&self, account_id: &str) -> Result<()> { self.request::( Method::DELETE, @@ -826,8 +724,10 @@ impl StalwartClient { Ok(()) } - - pub async fn list_principals(&self, principal_type: Option) -> Result> { + pub async fn list_principals( + &self, + principal_type: Option, + ) -> Result> { let path = match principal_type { Some(t) => format!("/api/principal?type={:?}", t).to_lowercase(), None => "/api/principal".to_string(), @@ -835,7 +735,6 @@ impl StalwartClient { self.request(Method::GET, &path, None).await } - pub async fn add_members(&self, account_id: &str, members: Vec) -> Result<()> { let updates: Vec = members .into_iter() @@ -844,7 +743,6 @@ impl StalwartClient { self.update_account(account_id, updates).await } - pub async fn remove_members(&self, account_id: &str, members: Vec) -> Result<()> { let updates: Vec = members .into_iter() @@ -853,11 +751,6 @@ impl StalwartClient { self.update_account(account_id, updates).await } - - - - - pub async fn set_auto_responder( &self, account_id: &str, @@ -879,7 +772,6 @@ impl StalwartClient { Ok(script_id) } - pub async fn disable_auto_responder(&self, account_id: &str) -> Result<()> { let script_id = format!("{}_vacation", account_id); @@ -895,10 +787,9 @@ impl StalwartClient { Ok(()) } - pub fn generate_vacation_sieve(&self, config: &AutoResponderConfig) -> String { - let mut script = String::from("require [\"vacation\", \"variables\", \"date\", \"relational\"];\n\n"); - + let mut script = + String::from("require [\"vacation\", \"variables\", \"date\", \"relational\"];\n\n"); if config.start_date.is_some() || config.end_date.is_some() { script.push_str("# Date-based activation\n"); @@ -920,7 +811,6 @@ impl StalwartClient { script.push('\n'); } - let subject = config.subject.replace('"', "\\\"").replace('\n', " "); let body = config.body_plain.replace('"', "\\\"").replace('\n', "\\n"); @@ -932,7 +822,6 @@ impl StalwartClient { script } - pub async fn set_filter_rule(&self, account_id: &str, rule: &EmailRule) -> Result { let sieve_script = self.generate_filter_sieve(rule); let script_id = format!("{}_filter_{}", account_id, rule.id); @@ -950,7 +839,6 @@ impl StalwartClient { Ok(script_id) } - pub async fn delete_filter_rule(&self, account_id: &str, rule_id: &str) -> Result<()> { let script_id = format!("{}_filter_{}", account_id, rule_id); @@ -966,10 +854,10 @@ impl StalwartClient { Ok(()) } - pub fn generate_filter_sieve(&self, rule: &EmailRule) -> String { - let mut script = - String::from("require [\"fileinto\", \"reject\", \"vacation\", \"imap4flags\", \"copy\"];\n\n"); + let mut script = String::from( + "require [\"fileinto\", \"reject\", \"vacation\", \"imap4flags\", \"copy\"];\n\n", + ); script.push_str(&format!("# Rule: {}\n", rule.name)); @@ -978,7 +866,6 @@ impl StalwartClient { return script; } - let mut conditions = Vec::new(); for condition in &rule.conditions { let cond_str = self.generate_condition_sieve(condition); @@ -988,14 +875,11 @@ impl StalwartClient { } if conditions.is_empty() { - script.push_str("# Always applies\n"); } else { - script.push_str(&format!("if allof ({}) {{\n", conditions.join(", "))); } - for action in &rule.actions { let action_str = self.generate_action_sieve(action); if !action_str.is_empty() { @@ -1007,7 +891,6 @@ impl StalwartClient { } } - if rule.stop_processing { if conditions.is_empty() { script.push_str("stop;\n"); @@ -1016,7 +899,6 @@ impl StalwartClient { } } - if !conditions.is_empty() { script.push_str("}\n"); } @@ -1024,7 +906,6 @@ impl StalwartClient { script } - pub fn generate_condition_sieve(&self, condition: &RuleCondition) -> String { let field_header = match condition.field.as_str() { "from" => "From", @@ -1044,17 +925,34 @@ impl StalwartClient { let value = condition.value.replace('"', "\\\""); match condition.operator.as_str() { - "contains" => format!("header :contains{} \"{}\" \"{}\"", comparator, field_header, value), - "equals" => format!("header :is{} \"{}\" \"{}\"", comparator, field_header, value), - "startsWith" => format!("header :matches{} \"{}\" \"{}*\"", comparator, field_header, value), - "endsWith" => format!("header :matches{} \"{}\" \"*{}\"", comparator, field_header, value), - "regex" => format!("header :regex{} \"{}\" \"{}\"", comparator, field_header, value), - "notContains" => format!("not header :contains{} \"{}\" \"{}\"", comparator, field_header, value), + "contains" => format!( + "header :contains{} \"{}\" \"{}\"", + comparator, field_header, value + ), + "equals" => format!( + "header :is{} \"{}\" \"{}\"", + comparator, field_header, value + ), + "startsWith" => format!( + "header :matches{} \"{}\" \"{}*\"", + comparator, field_header, value + ), + "endsWith" => format!( + "header :matches{} \"{}\" \"*{}\"", + comparator, field_header, value + ), + "regex" => format!( + "header :regex{} \"{}\" \"{}\"", + comparator, field_header, value + ), + "notContains" => format!( + "not header :contains{} \"{}\" \"{}\"", + comparator, field_header, value + ), _ => String::new(), } } - pub fn generate_action_sieve(&self, action: &RuleAction) -> String { match action.action_type.as_str() { "move" => format!("fileinto \"{}\";", action.value.replace('"', "\\\"")), @@ -1068,16 +966,11 @@ impl StalwartClient { } } - - - - - pub async fn get_metrics(&self) -> Result { - self.request(Method::GET, "/api/telemetry/metrics", None).await + self.request(Method::GET, "/api/telemetry/metrics", None) + .await } - pub async fn get_logs(&self, page: u32, limit: u32) -> Result { self.request( Method::GET, @@ -1087,7 +980,6 @@ impl StalwartClient { .await } - pub async fn get_logs_by_level(&self, level: &str, page: u32, limit: u32) -> Result { self.request( Method::GET, @@ -1097,7 +989,6 @@ impl StalwartClient { .await } - pub async fn get_traces(&self, trace_type: &str, page: u32) -> Result { self.request( Method::GET, @@ -1110,7 +1001,6 @@ impl StalwartClient { .await } - pub async fn get_recent_traces(&self, limit: u32) -> Result { self.request( Method::GET, @@ -1120,7 +1010,6 @@ impl StalwartClient { .await } - pub async fn get_trace(&self, trace_id: &str) -> Result> { self.request( Method::GET, @@ -1130,7 +1019,6 @@ impl StalwartClient { .await } - pub async fn get_dmarc_reports(&self, page: u32) -> Result { self.request( Method::GET, @@ -1140,7 +1028,6 @@ impl StalwartClient { .await } - pub async fn get_tls_reports(&self, page: u32) -> Result { self.request( Method::GET, @@ -1150,7 +1037,6 @@ impl StalwartClient { .await } - pub async fn get_arf_reports(&self, page: u32) -> Result { self.request( Method::GET, @@ -1160,23 +1046,16 @@ impl StalwartClient { .await } - pub async fn get_live_metrics_token(&self) -> Result { self.request(Method::GET, "/api/telemetry/live/metrics-token", None) .await } - pub async fn get_live_tracing_token(&self) -> Result { self.request(Method::GET, "/api/telemetry/live/tracing-token", None) .await } - - - - - pub async fn train_spam(&self, raw_message: &str) -> Result<()> { self.request_raw( Method::POST, @@ -1189,7 +1068,6 @@ impl StalwartClient { Ok(()) } - pub async fn train_ham(&self, raw_message: &str) -> Result<()> { self.request_raw( Method::POST, @@ -1202,8 +1080,10 @@ impl StalwartClient { Ok(()) } - - pub async fn classify_message(&self, message: &SpamClassifyRequest) -> Result { + pub async fn classify_message( + &self, + message: &SpamClassifyRequest, + ) -> Result { self.request( Method::POST, "/api/spam-filter/classify", @@ -1212,21 +1092,18 @@ impl StalwartClient { .await } - - - - - pub async fn troubleshoot_delivery(&self, recipient: &str) -> Result { self.request( Method::GET, - &format!("/api/troubleshoot/delivery/{}", urlencoding::encode(recipient)), + &format!( + "/api/troubleshoot/delivery/{}", + urlencoding::encode(recipient) + ), None, ) .await } - pub async fn check_dmarc(&self, domain: &str, from_email: &str) -> Result { let body = json!({ "domain": domain, @@ -1236,17 +1113,11 @@ impl StalwartClient { .await } - pub async fn get_dns_records(&self, domain: &str) -> Result { - self.request( - Method::GET, - &format!("/api/dns/records/{}", domain), - None, - ) - .await + self.request(Method::GET, &format!("/api/dns/records/{}", domain), None) + .await } - pub async fn undelete_messages(&self, account_id: &str) -> Result { self.request( Method::POST, @@ -1256,7 +1127,6 @@ impl StalwartClient { .await } - pub async fn purge_account(&self, account_id: &str) -> Result<()> { self.request::( Method::GET, @@ -1268,9 +1138,11 @@ impl StalwartClient { Ok(()) } - pub async fn health_check(&self) -> Result { - match self.request::(Method::GET, "/api/queue/status", None).await { + match self + .request::(Method::GET, "/api/queue/status", None) + .await + { Ok(_) => Ok(true), Err(e) => { warn!("Stalwart health check failed: {}", e); @@ -1279,5 +1151,3 @@ impl StalwartClient { } } } - - diff --git a/src/email/vectordb.rs b/src/email/vectordb.rs index da8e08137..258b4b736 100644 --- a/src/email/vectordb.rs +++ b/src/email/vectordb.rs @@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use tokio::fs; - use uuid::Uuid; #[cfg(feature = "vectordb")] @@ -13,7 +12,6 @@ use qdrant_client::{ Qdrant, }; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailDocument { pub id: String, @@ -29,7 +27,6 @@ pub struct EmailDocument { pub thread_id: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailSearchQuery { pub query_text: String, @@ -40,7 +37,6 @@ pub struct EmailSearchQuery { pub limit: usize, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailSearchResult { pub email: EmailDocument, @@ -48,7 +44,6 @@ pub struct EmailSearchResult { pub snippet: String, } - pub struct UserEmailVectorDB { user_id: Uuid, bot_id: Uuid, @@ -59,7 +54,6 @@ pub struct UserEmailVectorDB { } impl UserEmailVectorDB { - pub fn new(user_id: Uuid, bot_id: Uuid, db_path: PathBuf) -> Self { let collection_name = format!("emails_{}_{}", bot_id, user_id); @@ -73,12 +67,10 @@ impl UserEmailVectorDB { } } - #[cfg(feature = "vectordb")] pub async fn initialize(&mut self, qdrant_url: &str) -> Result<()> { let client = Qdrant::from_url(qdrant_url).build()?; - let collections = client.list_collections().await?; let exists = collections .collections @@ -86,7 +78,6 @@ impl UserEmailVectorDB { .any(|c| c.name == self.collection_name); if !exists { - client .create_collection( qdrant_client::qdrant::CreateCollectionBuilder::new(&self.collection_name) @@ -111,7 +102,6 @@ impl UserEmailVectorDB { Ok(()) } - #[cfg(feature = "vectordb")] pub async fn index_email(&self, email: &EmailDocument, embedding: Vec) -> Result<()> { let client = self @@ -131,9 +121,10 @@ impl UserEmailVectorDB { let point = PointStruct::new(email.id.clone(), embedding, payload); client - .upsert_points( - qdrant_client::qdrant::UpsertPointsBuilder::new(&self.collection_name, vec![point]), - ) + .upsert_points(qdrant_client::qdrant::UpsertPointsBuilder::new( + &self.collection_name, + vec![point], + )) .await?; log::debug!("Indexed email: {} - {}", email.id, email.subject); @@ -142,14 +133,12 @@ impl UserEmailVectorDB { #[cfg(not(feature = "vectordb"))] pub async fn index_email(&self, email: &EmailDocument, _embedding: Vec) -> Result<()> { - let file_path = self.db_path.join(format!("{}.json", email.id)); let json = serde_json::to_string_pretty(email)?; fs::write(file_path, json).await?; Ok(()) } - pub async fn index_emails_batch(&self, emails: &[(EmailDocument, Vec)]) -> Result<()> { for (email, embedding) in emails { self.index_email(email, embedding.clone()).await?; @@ -157,7 +146,6 @@ impl UserEmailVectorDB { Ok(()) } - #[cfg(feature = "vectordb")] pub async fn search( &self, @@ -169,7 +157,6 @@ impl UserEmailVectorDB { .as_ref() .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; - let filter = if query.account_id.is_some() || query.folder.is_some() { let mut conditions = vec![]; @@ -210,7 +197,8 @@ impl UserEmailVectorDB { let payload = &point.payload; if !payload.is_empty() { let get_str = |key: &str| -> String { - payload.get(key) + payload + .get(key) .and_then(|v| v.as_str()) .map(|s| s.to_string()) .unwrap_or_default() @@ -227,10 +215,12 @@ impl UserEmailVectorDB { date: chrono::Utc::now(), folder: get_str("folder"), has_attachments: false, - thread_id: payload.get("thread_id").and_then(|v| v.as_str()).map(|s| s.to_string()), + thread_id: payload + .get("thread_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), }; - let snippet = if email.body_text.len() > 200 { format!("{}...", &email.body_text[..200]) } else { @@ -254,7 +244,6 @@ impl UserEmailVectorDB { query: &EmailSearchQuery, _query_embedding: Vec, ) -> Result> { - let mut results = Vec::new(); let mut entries = fs::read_dir(&self.db_path).await?; @@ -262,7 +251,6 @@ impl UserEmailVectorDB { if entry.path().extension().and_then(|s| s.to_str()) == Some("json") { let content = fs::read_to_string(entry.path()).await?; if let Ok(email) = serde_json::from_str::(&content) { - let query_lower = query.query_text.to_lowercase(); if email.subject.to_lowercase().contains(&query_lower) || email.body_text.to_lowercase().contains(&query_lower) @@ -291,7 +279,6 @@ impl UserEmailVectorDB { Ok(results) } - #[cfg(feature = "vectordb")] pub async fn delete_email(&self, email_id: &str) -> Result<()> { let client = self @@ -301,8 +288,9 @@ impl UserEmailVectorDB { client .delete_points( - qdrant_client::qdrant::DeletePointsBuilder::new(&self.collection_name) - .points(vec![qdrant_client::qdrant::PointId::from(email_id.to_string())]), + qdrant_client::qdrant::DeletePointsBuilder::new(&self.collection_name).points( + vec![qdrant_client::qdrant::PointId::from(email_id.to_string())], + ), ) .await?; @@ -319,7 +307,6 @@ impl UserEmailVectorDB { Ok(()) } - #[cfg(feature = "vectordb")] pub async fn get_count(&self) -> Result { let client = self @@ -346,7 +333,6 @@ impl UserEmailVectorDB { Ok(count) } - #[cfg(feature = "vectordb")] pub async fn clear(&self) -> Result<()> { let client = self @@ -354,10 +340,7 @@ impl UserEmailVectorDB { .as_ref() .ok_or_else(|| anyhow::anyhow!("Vector DB not initialized"))?; - client - .delete_collection(&self.collection_name) - .await?; - + client.delete_collection(&self.collection_name).await?; client .create_collection( @@ -384,7 +367,6 @@ impl UserEmailVectorDB { } } - pub struct EmailEmbeddingGenerator { pub llm_endpoint: String, } @@ -394,15 +376,12 @@ impl EmailEmbeddingGenerator { Self { llm_endpoint } } - pub async fn generate_embedding(&self, email: &EmailDocument) -> Result> { - let text = format!( "From: {} <{}>\nSubject: {}\n\n{}", email.from_name, email.from_email, email.subject, email.body_text ); - let text = if text.len() > 8000 { &text[..8000] } else { @@ -412,17 +391,11 @@ impl EmailEmbeddingGenerator { self.generate_text_embedding(text).await } - pub async fn generate_text_embedding(&self, text: &str) -> Result> { - let embedding_url = "http://localhost:8082".to_string(); - return self.generate_local_embedding(text, &embedding_url).await; - - - self.generate_hash_embedding(text) + self.generate_local_embedding(text, &embedding_url).await } - async fn generate_openai_embedding(&self, text: &str, api_key: &str) -> Result> { use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use serde_json::json; @@ -462,7 +435,6 @@ impl EmailEmbeddingGenerator { Ok(embedding) } - async fn generate_local_embedding(&self, text: &str, embedding_url: &str) -> Result> { use serde_json::json; @@ -492,7 +464,6 @@ impl EmailEmbeddingGenerator { Ok(embedding) } - fn generate_hash_embedding(&self, text: &str) -> Result> { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -500,7 +471,6 @@ impl EmailEmbeddingGenerator { const EMBEDDING_DIM: usize = 1536; let mut embedding = vec![0.0f32; EMBEDDING_DIM]; - let words: Vec<&str> = text.split_whitespace().collect(); for (i, chunk) in words.chunks(10).enumerate() { @@ -508,7 +478,6 @@ impl EmailEmbeddingGenerator { chunk.join(" ").hash(&mut hasher); let hash = hasher.finish(); - for j in 0..64 { let idx = (i * 64 + j) % EMBEDDING_DIM; let value = ((hash >> j) & 1) as f32; @@ -516,7 +485,6 @@ impl EmailEmbeddingGenerator { } } - let norm: f32 = embedding.iter().map(|x| x * x).sum::().sqrt(); if norm > 0.0 { for val in &mut embedding { diff --git a/src/llm/observability.rs b/src/llm/observability.rs index e3491dc1b..6da3d09e7 100644 --- a/src/llm/observability.rs +++ b/src/llm/observability.rs @@ -1,36 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - use chrono::{DateTime, Utc}; use rhai::{Dynamic, Engine, Map}; use serde::{Deserialize, Serialize}; @@ -41,10 +8,8 @@ use tokio::sync::RwLock; use tracing::info; use uuid::Uuid; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LLMRequestMetrics { - pub request_id: Uuid, pub session_id: Uuid, @@ -78,7 +43,6 @@ pub struct LLMRequestMetrics { pub metadata: HashMap, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "lowercase")] pub enum RequestType { @@ -113,10 +77,8 @@ impl std::fmt::Display for RequestType { } } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AggregatedMetrics { - pub period_start: DateTime, pub period_end: DateTime, @@ -162,10 +124,8 @@ pub struct AggregatedMetrics { pub errors_by_type: HashMap, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelPricing { - pub model: String, pub input_cost_per_1k: f64, @@ -189,10 +149,8 @@ impl Default for ModelPricing { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Budget { - pub daily_limit: f64, pub monthly_limit: f64, @@ -229,10 +187,8 @@ impl Default for Budget { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TraceEvent { - pub id: Uuid, pub parent_id: Option, @@ -258,7 +214,6 @@ pub struct TraceEvent { pub error: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum TraceEventType { @@ -268,7 +223,6 @@ pub enum TraceEventType { Metric, } - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum TraceStatus { @@ -283,10 +237,8 @@ impl Default for TraceStatus { } } - #[derive(Debug, Clone)] pub struct ObservabilityConfig { - pub enabled: bool, pub metrics_interval: u64, @@ -322,11 +274,9 @@ impl Default for ObservabilityConfig { } } - fn default_model_pricing() -> HashMap { let mut pricing = HashMap::new(); - pricing.insert( "gpt-4".to_string(), ModelPricing { @@ -360,7 +310,6 @@ fn default_model_pricing() -> HashMap { }, ); - pricing.insert( "claude-3-opus".to_string(), ModelPricing { @@ -383,7 +332,6 @@ fn default_model_pricing() -> HashMap { }, ); - pricing.insert( "mixtral-8x7b-32768".to_string(), ModelPricing { @@ -395,7 +343,6 @@ fn default_model_pricing() -> HashMap { }, ); - pricing.insert( "local".to_string(), ModelPricing { @@ -410,7 +357,6 @@ fn default_model_pricing() -> HashMap { pricing } - #[derive(Debug)] pub struct ObservabilityManager { config: ObservabilityConfig, @@ -431,7 +377,6 @@ pub struct ObservabilityManager { } impl ObservabilityManager { - pub fn new(config: ObservabilityConfig) -> Self { let budget = Budget { daily_limit: config.budget_daily, @@ -454,7 +399,6 @@ impl ObservabilityManager { } } - pub fn from_config(config_map: &HashMap) -> Self { let config = ObservabilityConfig { enabled: config_map @@ -494,13 +438,11 @@ impl ObservabilityManager { ObservabilityManager::new(config) } - pub async fn record_request(&self, metrics: LLMRequestMetrics) { if !self.config.enabled { return; } - self.request_count.fetch_add(1, Ordering::Relaxed); self.token_count .fetch_add(metrics.total_tokens, Ordering::Relaxed); @@ -515,24 +457,20 @@ impl ObservabilityManager { self.error_count.fetch_add(1, Ordering::Relaxed); } - if self.config.cost_tracking && metrics.estimated_cost > 0.0 { let mut budget = self.budget.write().await; budget.daily_spend += metrics.estimated_cost; budget.monthly_spend += metrics.estimated_cost; } - let mut buffer = self.metrics_buffer.write().await; buffer.push(metrics); - if buffer.len() > 10000 { buffer.drain(0..1000); } } - pub fn calculate_cost(&self, model: &str, input_tokens: u64, output_tokens: u64) -> f64 { if let Some(pricing) = self.config.model_pricing.get(model) { if pricing.is_local { @@ -542,12 +480,10 @@ impl ObservabilityManager { let output_cost = (output_tokens as f64 / 1000.0) * pricing.output_cost_per_1k; input_cost + output_cost + pricing.cost_per_request } else { - 0.0 } } - pub async fn get_budget_status(&self) -> BudgetStatus { let budget = self.budget.read().await; BudgetStatus { @@ -567,7 +503,6 @@ impl ObservabilityManager { } } - pub async fn check_budget(&self, estimated_cost: f64) -> BudgetCheckResult { let budget = self.budget.read().await; @@ -593,7 +528,6 @@ impl ObservabilityManager { BudgetCheckResult::Ok } - pub async fn reset_daily_budget(&self) { let mut budget = self.budget.write().await; budget.daily_spend = 0.0; @@ -601,7 +535,6 @@ impl ObservabilityManager { budget.daily_alert_sent = false; } - pub async fn reset_monthly_budget(&self) { let mut budget = self.budget.write().await; budget.monthly_spend = 0.0; @@ -609,13 +542,11 @@ impl ObservabilityManager { budget.monthly_alert_sent = false; } - pub async fn record_trace(&self, event: TraceEvent) { if !self.config.enabled || !self.config.trace_enabled { return; } - if self.config.trace_sample_rate < 1.0 { let sample: f64 = rand::random(); if sample > self.config.trace_sample_rate { @@ -626,13 +557,11 @@ impl ObservabilityManager { let mut buffer = self.trace_buffer.write().await; buffer.push(event); - if buffer.len() > 5000 { buffer.drain(0..500); } } - pub fn start_span( &self, trace_id: Uuid, @@ -656,7 +585,6 @@ impl ObservabilityManager { } } - pub fn end_span(&self, span: &mut TraceEvent, status: TraceStatus, error: Option) { let end_time = Utc::now(); span.end_time = Some(end_time); @@ -665,7 +593,6 @@ impl ObservabilityManager { span.error = error; } - pub async fn get_aggregated_metrics( &self, start: DateTime, @@ -731,7 +658,6 @@ impl ObservabilityManager { .or_insert(0) += 1; } - if !latencies.is_empty() { latencies.sort(); let len = latencies.len(); @@ -748,7 +674,6 @@ impl ObservabilityManager { metrics } - pub async fn get_current_metrics(&self) -> AggregatedMetrics { self.current_metrics.read().await.clone() } @@ -789,13 +714,11 @@ impl ObservabilityManager { } } - pub async fn get_recent_traces(&self, limit: usize) -> Vec { let buffer = self.trace_buffer.read().await; buffer.iter().rev().take(limit).cloned().collect() } - pub async fn get_trace(&self, trace_id: Uuid) -> Vec { let buffer = self.trace_buffer.read().await; buffer @@ -806,7 +729,6 @@ impl ObservabilityManager { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetStatus { pub daily_limit: f64, @@ -823,7 +745,6 @@ pub struct BudgetStatus { pub near_monthly_limit: bool, } - #[derive(Debug, Clone, PartialEq)] pub enum BudgetCheckResult { Ok, @@ -833,7 +754,6 @@ pub enum BudgetCheckResult { MonthlyExceeded, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QuickStats { pub total_requests: u64, @@ -845,7 +765,6 @@ pub struct QuickStats { pub error_rate: f64, } - impl LLMRequestMetrics { pub fn to_dynamic(&self) -> Dynamic { let mut map = Map::new(); @@ -935,10 +854,7 @@ impl BudgetStatus { } } - pub fn register_observability_keywords(engine: &mut Engine) { - - engine.register_fn("metrics_total_requests", |metrics: Map| -> i64 { metrics .get("total_requests") @@ -1006,8 +922,7 @@ pub fn register_observability_keywords(engine: &mut Engine) { info!("Observability keywords registered"); } - -pub const OBSERVABILITY_SCHEMA: &str = r#" +pub const OBSERVABILITY_SCHEMA: &str = r" -- LLM request metrics CREATE TABLE IF NOT EXISTS llm_metrics ( id UUID PRIMARY KEY, @@ -1102,4 +1017,4 @@ CREATE INDEX IF NOT EXISTS idx_llm_metrics_hourly_hour ON llm_metrics_hourly(hou CREATE INDEX IF NOT EXISTS idx_llm_traces_trace_id ON llm_traces(trace_id); CREATE INDEX IF NOT EXISTS idx_llm_traces_start_time ON llm_traces(start_time DESC); CREATE INDEX IF NOT EXISTS idx_llm_traces_component ON llm_traces(component); -"#; +"; diff --git a/src/msteams/mod.rs b/src/msteams/mod.rs index e726a9892..b726b4ab0 100644 --- a/src/msteams/mod.rs +++ b/src/msteams/mod.rs @@ -50,7 +50,7 @@ pub fn configure() -> Router> { } async fn handle_incoming( - State(state): State>, + State(_state): State>, Json(activity): Json, ) -> impl IntoResponse { match activity.activity_type.as_str() { diff --git a/src/security/cert_pinning.rs b/src/security/cert_pinning.rs index ee7c314f4..2a29db46c 100644 --- a/src/security/cert_pinning.rs +++ b/src/security/cert_pinning.rs @@ -1,30 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - use anyhow::{anyhow, Context, Result}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use reqwest::{Certificate, Client, ClientBuilder}; @@ -38,28 +11,20 @@ use std::time::Duration; use tracing::{debug, error, info, warn}; use x509_parser::prelude::*; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertPinningConfig { - pub enabled: bool, - pub pins: HashMap>, - pub require_pins: bool, - pub allow_backup_pins: bool, - pub report_only: bool, - pub config_path: Option, - pub cache_ttl_secs: u64, } @@ -78,12 +43,10 @@ impl Default for CertPinningConfig { } impl CertPinningConfig { - pub fn new() -> Self { Self::default() } - pub fn strict() -> Self { Self { enabled: true, @@ -96,7 +59,6 @@ impl CertPinningConfig { } } - pub fn report_only() -> Self { Self { enabled: true, @@ -109,28 +71,23 @@ impl CertPinningConfig { } } - pub fn add_pin(&mut self, pin: PinnedCert) { let hostname = pin.hostname.clone(); self.pins.entry(hostname).or_default().push(pin); } - pub fn add_pins(&mut self, hostname: &str, pins: Vec) { self.pins.insert(hostname.to_string(), pins); } - pub fn remove_pins(&mut self, hostname: &str) { self.pins.remove(hostname); } - pub fn get_pins(&self, hostname: &str) -> Option<&Vec> { self.pins.get(hostname) } - pub fn load_from_file(path: &Path) -> Result { let content = fs::read_to_string(path) .with_context(|| format!("Failed to read pin config from {:?}", path))?; @@ -142,7 +99,6 @@ impl CertPinningConfig { Ok(config) } - pub fn save_to_file(&self, path: &Path) -> Result<()> { let content = serde_json::to_string_pretty(self).context("Failed to serialize pin configuration")?; @@ -155,31 +111,22 @@ impl CertPinningConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PinnedCert { - pub hostname: String, - - pub fingerprint: String, - pub description: Option, - pub is_backup: bool, - pub expires_at: Option>, - pub pin_type: PinType, } impl PinnedCert { - pub fn new(hostname: &str, fingerprint: &str) -> Self { Self { hostname: hostname.to_string(), @@ -191,7 +138,6 @@ impl PinnedCert { } } - pub fn backup(hostname: &str, fingerprint: &str) -> Self { Self { hostname: hostname.to_string(), @@ -203,25 +149,21 @@ impl PinnedCert { } } - pub fn with_type(mut self, pin_type: PinType) -> Self { self.pin_type = pin_type; self } - pub fn with_description(mut self, desc: &str) -> Self { self.description = Some(desc.to_string()); self } - pub fn with_expiration(mut self, expires: chrono::DateTime) -> Self { self.expires_at = Some(expires); self } - pub fn get_hash_bytes(&self) -> Result> { let hash_str = self .fingerprint @@ -233,7 +175,6 @@ impl PinnedCert { .context("Failed to decode base64 fingerprint") } - pub fn verify(&self, cert_der: &[u8]) -> Result { let expected_hash = self.get_hash_bytes()?; let actual_hash = compute_spki_fingerprint(cert_der)?; @@ -242,10 +183,8 @@ impl PinnedCert { } } - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum PinType { - Leaf, Intermediate, @@ -259,30 +198,22 @@ impl Default for PinType { } } - #[derive(Debug, Clone)] pub struct PinValidationResult { - pub valid: bool, - pub hostname: String, - pub matched_pin: Option, - pub actual_fingerprint: String, - pub error: Option, - pub backup_match: bool, } impl PinValidationResult { - pub fn success(hostname: &str, fingerprint: &str, backup: bool) -> Self { Self { valid: true, @@ -294,7 +225,6 @@ impl PinValidationResult { } } - pub fn failure(hostname: &str, actual: &str, error: &str) -> Self { Self { valid: false, @@ -307,7 +237,6 @@ impl PinValidationResult { } } - #[derive(Debug)] pub struct CertPinningManager { config: Arc>, @@ -315,7 +244,6 @@ pub struct CertPinningManager { } impl CertPinningManager { - pub fn new(config: CertPinningConfig) -> Self { Self { config: Arc::new(RwLock::new(config)), @@ -323,17 +251,14 @@ impl CertPinningManager { } } - pub fn default_manager() -> Self { Self::new(CertPinningConfig::default()) } - pub fn is_enabled(&self) -> bool { self.config.read().unwrap().enabled } - pub fn add_pin(&self, pin: PinnedCert) -> Result<()> { let mut config = self .config @@ -343,7 +268,6 @@ impl CertPinningManager { Ok(()) } - pub fn remove_pins(&self, hostname: &str) -> Result<()> { let mut config = self .config @@ -351,7 +275,6 @@ impl CertPinningManager { .map_err(|_| anyhow!("Failed to acquire write lock"))?; config.remove_pins(hostname); - let mut cache = self .validation_cache .write() @@ -361,7 +284,6 @@ impl CertPinningManager { Ok(()) } - pub fn validate_certificate( &self, hostname: &str, @@ -376,7 +298,6 @@ impl CertPinningManager { return Ok(PinValidationResult::success(hostname, "disabled", false)); } - if let Ok(cache) = self.validation_cache.read() { if let Some((result, timestamp)) = cache.get(hostname) { if timestamp.elapsed().as_secs() < config.cache_ttl_secs { @@ -385,11 +306,9 @@ impl CertPinningManager { } } - let actual_hash = compute_spki_fingerprint(cert_der)?; let actual_fingerprint = format!("sha256//{}", BASE64.encode(&actual_hash)); - let pins = match config.get_pins(hostname) { Some(pins) => pins, None => { @@ -411,7 +330,6 @@ impl CertPinningManager { return Ok(result); } - return Ok(PinValidationResult::success( hostname, "no-pins-required", @@ -420,7 +338,6 @@ impl CertPinningManager { } }; - for pin in pins { match pin.verify(cert_der) { Ok(true) => { @@ -435,7 +352,6 @@ impl CertPinningManager { ); } - if let Ok(mut cache) = self.validation_cache.write() { cache.insert( hostname.to_string(), @@ -445,15 +361,13 @@ impl CertPinningManager { return Ok(result); } - Ok(false) => continue, + Ok(false) => {} Err(e) => { debug!("Pin verification error for {}: {}", hostname, e); - continue; } } } - let result = PinValidationResult::failure( hostname, &actual_fingerprint, @@ -479,12 +393,10 @@ impl CertPinningManager { Ok(result) } - pub fn create_pinned_client(&self, hostname: &str) -> Result { self.create_pinned_client_with_options(hostname, None, Duration::from_secs(30)) } - pub fn create_pinned_client_with_options( &self, hostname: &str, @@ -503,14 +415,10 @@ impl CertPinningManager { .https_only(true) .tls_built_in_root_certs(true); - if let Some(cert) = ca_cert { builder = builder.add_root_certificate(cert.clone()); } - - - if config.enabled && config.get_pins(hostname).is_some() { debug!( "Creating pinned client for {} with {} pins", @@ -522,7 +430,6 @@ impl CertPinningManager { builder.build().context("Failed to build HTTP client") } - pub fn validate_pem_file( &self, hostname: &str, @@ -535,12 +442,10 @@ impl CertPinningManager { self.validate_certificate(hostname, &der) } - pub fn generate_pin_from_file(hostname: &str, cert_path: &Path) -> Result { let cert_data = fs::read(cert_path) .with_context(|| format!("Failed to read certificate: {:?}", cert_path))?; - let der = if cert_data.starts_with(b"-----BEGIN") { pem_to_der(&cert_data)? } else { @@ -553,7 +458,6 @@ impl CertPinningManager { Ok(PinnedCert::new(hostname, &fingerprint_str)) } - pub fn generate_pins_from_directory( hostname: &str, cert_dir: &Path, @@ -583,7 +487,6 @@ impl CertPinningManager { Ok(pins) } - pub fn export_pins(&self, path: &Path) -> Result<()> { let config = self .config @@ -593,7 +496,6 @@ impl CertPinningManager { config.save_to_file(path) } - pub fn import_pins(&self, path: &Path) -> Result<()> { let imported = CertPinningConfig::load_from_file(path)?; @@ -606,7 +508,6 @@ impl CertPinningManager { config.pins.insert(hostname, pins); } - if let Ok(mut cache) = self.validation_cache.write() { cache.clear(); } @@ -614,7 +515,6 @@ impl CertPinningManager { Ok(()) } - pub fn get_stats(&self) -> Result { let config = self .config @@ -653,7 +553,6 @@ impl CertPinningManager { } } - #[derive(Debug, Clone, Serialize)] pub struct PinningStats { pub enabled: bool, @@ -664,31 +563,25 @@ pub struct PinningStats { pub report_only: bool, } - pub fn compute_spki_fingerprint(cert_der: &[u8]) -> Result> { let (_, cert) = X509Certificate::from_der(cert_der) .map_err(|e| anyhow!("Failed to parse X.509 certificate: {}", e))?; - let spki = cert.public_key().raw; - let hash = digest(&SHA256, spki); Ok(hash.as_ref().to_vec()) } - pub fn compute_cert_fingerprint(cert_der: &[u8]) -> Vec { let hash = digest(&SHA256, cert_der); hash.as_ref().to_vec() } - pub fn pem_to_der(pem_data: &[u8]) -> Result> { let pem_str = std::str::from_utf8(pem_data).context("Invalid UTF-8 in PEM data")?; - let start_marker = "-----BEGIN CERTIFICATE-----"; let end_marker = "-----END CERTIFICATE-----"; @@ -708,7 +601,6 @@ pub fn pem_to_der(pem_data: &[u8]) -> Result> { .context("Failed to decode base64 certificate data") } - pub fn format_fingerprint(hash: &[u8]) -> String { hash.iter() .map(|b| format!("{:02X}", b)) @@ -716,16 +608,13 @@ pub fn format_fingerprint(hash: &[u8]) -> String { .join(":") } - pub fn parse_fingerprint(formatted: &str) -> Result> { - if let Some(base64_part) = formatted.strip_prefix("sha256//") { return BASE64 .decode(base64_part) .context("Failed to decode base64 fingerprint"); } - if formatted.contains(':') { let bytes: Result, _> = formatted .split(':') @@ -735,7 +624,6 @@ pub fn parse_fingerprint(formatted: &str) -> Result> { return bytes.context("Failed to parse hex fingerprint"); } - let bytes: Result, _> = (0..formatted.len()) .step_by(2) .map(|i| u8::from_str_radix(&formatted[i..i + 2], 16)) diff --git a/src/tasks/scheduler.rs b/src/tasks/scheduler.rs index 859cd3aab..dbfdd7cdd 100644 --- a/src/tasks/scheduler.rs +++ b/src/tasks/scheduler.rs @@ -83,7 +83,7 @@ type TaskHandler = Arc< impl TaskScheduler { pub fn new(state: Arc) -> Self { let scheduler = Self { - state: state, + state, running_tasks: Arc::new(RwLock::new(HashMap::new())), task_registry: Arc::new(RwLock::new(HashMap::new())), scheduled_tasks: Arc::new(RwLock::new(Vec::new())), @@ -101,14 +101,10 @@ impl TaskScheduler { tokio::spawn(async move { let mut handlers = registry.write().await; - handlers.insert( "database_cleanup".to_string(), Arc::new(move |_state: Arc, _payload: serde_json::Value| { Box::pin(async move { - - - info!("Database cleanup task executed"); Ok(serde_json::json!({ @@ -120,7 +116,6 @@ impl TaskScheduler { }), ); - handlers.insert( "cache_cleanup".to_string(), Arc::new(move |state: Arc, _payload: serde_json::Value| { @@ -139,7 +134,6 @@ impl TaskScheduler { }), ); - handlers.insert( "backup".to_string(), Arc::new(move |state: Arc, payload: serde_json::Value| { @@ -157,7 +151,6 @@ impl TaskScheduler { .arg(&backup_file) .output()?; - if state.s3_client.is_some() { let s3 = state.s3_client.as_ref().unwrap(); let body = tokio::fs::read(&backup_file).await?; @@ -196,7 +189,6 @@ impl TaskScheduler { }), ); - handlers.insert( "generate_report".to_string(), Arc::new(move |_state: Arc, payload: serde_json::Value| { @@ -229,7 +221,6 @@ impl TaskScheduler { }), ); - handlers.insert( "health_check".to_string(), Arc::new(move |state: Arc, _payload: serde_json::Value| { @@ -240,17 +231,14 @@ impl TaskScheduler { "timestamp": Utc::now() }); - let db_ok = state.conn.get().is_ok(); health["database"] = serde_json::json!(db_ok); - if let Some(cache) = &state.cache { let cache_ok = cache.get_connection().is_ok(); health["cache"] = serde_json::json!(cache_ok); } - if let Some(s3) = &state.s3_client { let s3_ok = s3.list_buckets().send().await.is_ok(); health["storage"] = serde_json::json!(s3_ok); @@ -351,7 +339,6 @@ impl TaskScheduler { let execution_id = Uuid::new_v4(); let started_at = Utc::now(); - let _execution = TaskExecution { id: execution_id, scheduled_task_id: task_id, @@ -363,11 +350,6 @@ impl TaskScheduler { duration_ms: None, }; - - - - - let result = { let handlers = registry.read().await; if let Some(handler) = handlers.get(&task.task_type) { @@ -388,25 +370,20 @@ impl TaskScheduler { let completed_at = Utc::now(); let _duration_ms = (completed_at - started_at).num_milliseconds(); - match result { Ok(_result) => { - let schedule = Schedule::from_str(&task.cron_expression).ok(); let _next_run = schedule .and_then(|s| s.upcoming(chrono::Local).take(1).next()) .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|| Utc::now() + Duration::hours(1)); - - info!("Task {} completed successfully", task.name); } Err(e) => { let error_msg = format!("Task failed: {}", e); error!("{}", error_msg); - task.retry_count += 1; if task.retry_count < task.max_retries { let _retry_delay = @@ -424,12 +401,10 @@ impl TaskScheduler { } } - let mut running = running_tasks.write().await; running.remove(&task_id); }); - let mut running = self.running_tasks.write().await; running.insert(task_id, handle); } @@ -445,7 +420,6 @@ impl TaskScheduler { info!("Stopped task: {}", task_id); } - let mut tasks = self.scheduled_tasks.write().await; if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) { task.enabled = false;