use crate::shared::message_types::MessageType; use crate::shared::models::{BotResponse, UserSession}; use crate::shared::state::AppState; use log::{error, trace}; use regex::Regex; use rhai::{Dynamic, Engine, EvalAltResult}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum InputType { Any, Email, Date, Name, Integer, Float, Boolean, Hour, Money, Mobile, Zipcode, Language, Cpf, Cnpj, QrCode, Login, Menu(Vec), File, Image, Audio, Video, Document, Url, Uuid, Color, CreditCard, Password, } impl InputType { pub fn error_message(&self) -> String { match self { InputType::Any => "".to_string(), InputType::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(), } } 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, } } } #[derive(Debug, Clone)] pub struct ValidationResult { pub is_valid: bool, pub normalized_value: String, pub error_message: Option, pub metadata: Option, } impl ValidationResult { pub fn valid(value: String) -> Self { Self { is_valid: true, normalized_value: value, error_message: None, metadata: None, } } pub fn valid_with_metadata(value: String, metadata: serde_json::Value) -> Self { Self { is_valid: true, normalized_value: value, error_message: None, metadata: Some(metadata), } } pub fn invalid(error: String) -> Self { Self { is_valid: false, normalized_value: String::new(), error_message: Some(error), metadata: None, } } } 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") .to_lowercase(); trace!( "HEAR command waiting for user input to store in variable: {}", variable_name ); let state_for_spawn = Arc::clone(&state_clone); let session_id_clone = session_id; let var_name_clone = variable_name.clone(); tokio::spawn(async move { trace!( "HEAR: Setting session {} to wait for input for variable '{}'", session_id_clone, var_name_clone ); 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 wait_data = serde_json::json!({ "variable": var_name_clone, "type": "any", "waiting": true, "retry_count": 0 }); let _: Result<(), _> = redis::cmd("SET") .arg(&key) .arg(wait_data.to_string()) .arg("EX") .arg(3600) .query_async(&mut conn) .await; } } }); Err(Box::new(EvalAltResult::ErrorRuntime( "Waiting for user input".into(), rhai::Position::NONE, ))) }) .unwrap(); } fn register_hear_as_type(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", "$ident$"], true, move |_context, inputs| { let variable_name = inputs[0] .get_string_value() .expect("Expected identifier for variable") .to_lowercase(); let type_name = inputs[1] .get_string_value() .expect("Expected identifier for type") .to_string(); let _input_type = InputType::from_str(&type_name); trace!( "HEAR {} AS {} - waiting for validated input", variable_name, type_name ); let state_for_spawn = Arc::clone(&state_clone); let session_id_clone = session_id; let var_name_clone = variable_name.clone(); let type_clone = type_name.clone(); tokio::spawn(async move { 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 wait_data = serde_json::json!({ "variable": var_name_clone, "type": type_clone.to_lowercase(), "waiting": true, "retry_count": 0, "max_retries": 3 }); let _: Result<(), _> = redis::cmd("SET") .arg(&key) .arg(wait_data.to_string()) .arg("EX") .arg(3600) .query_async(&mut conn) .await; } } }); Err(Box::new(EvalAltResult::ErrorRuntime( "Waiting for user input".into(), rhai::Position::NONE, ))) }, ) .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()) .filter(|s| !s.is_empty()) .collect() }; if options.is_empty() { return Err(Box::new(EvalAltResult::ErrorRuntime( "Menu requires at least one option".into(), rhai::Position::NONE, ))); } trace!("HEAR {} AS MENU with options: {:?}", variable_name, options); let state_for_spawn = Arc::clone(&state_clone); let session_id_clone = session_id; let var_name_clone = variable_name.clone(); let options_clone = options.clone(); tokio::spawn(async move { 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 wait_data = serde_json::json!({ "variable": var_name_clone, "type": "menu", "options": options_clone, "waiting": true, "retry_count": 0 }); let _: Result<(), _> = redis::cmd("SET") .arg(&key) .arg(wait_data.to_string()) .arg("EX") .arg(3600) .query_async(&mut conn) .await; let suggestions_key = format!("suggestions:{}:{}", session_id_clone, session_id_clone); for opt in &options_clone { let suggestion = serde_json::json!({ "text": opt, "value": opt }); let _: Result<(), _> = redis::cmd("RPUSH") .arg(&suggestions_key) .arg(suggestion.to_string()) .query_async(&mut conn) .await; } } } }); Err(Box::new(EvalAltResult::ErrorRuntime( "Waiting for user input".into(), rhai::Position::NONE, ))) }, ) .unwrap(); } 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::Email => validate_email(trimmed), InputType::Date => validate_date(trimmed), InputType::Name => validate_name(trimmed), InputType::Integer => validate_integer(trimmed), InputType::Float => validate_float(trimmed), InputType::Boolean => validate_boolean(trimmed), InputType::Hour => validate_hour(trimmed), InputType::Money => validate_money(trimmed), InputType::Mobile => validate_mobile(trimmed), InputType::Zipcode => validate_zipcode(trimmed), InputType::Language => validate_language(trimmed), InputType::Cpf => validate_cpf(trimmed), InputType::Cnpj => validate_cnpj(trimmed), InputType::Url => validate_url(trimmed), InputType::Uuid => validate_uuid(trimmed), 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()), } } fn validate_email(input: &str) -> ValidationResult { let email_regex = Regex::new( r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ).unwrap(); if email_regex.is_match(input) { ValidationResult::valid(input.to_lowercase()) } else { ValidationResult::invalid(InputType::Email.error_message()) } } 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 %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 }), ); } } let lower = input.to_lowercase(); let today = chrono::Local::now().date_naive(); if lower == "today" || lower == "hoje" { return ValidationResult::valid(today.format("%Y-%m-%d").to_string()); } if lower == "tomorrow" || lower == "amanhã" || lower == "amanha" { return ValidationResult::valid( (today + chrono::Duration::days(1)) .format("%Y-%m-%d") .to_string(), ); } if lower == "yesterday" || lower == "ontem" { return ValidationResult::valid( (today - chrono::Duration::days(1)) .format("%Y-%m-%d") .to_string(), ); } ValidationResult::invalid(InputType::Date.error_message()) } fn validate_name(input: &str) -> ValidationResult { let name_regex = Regex::new(r"^[\p{L}\s\-']+$").unwrap(); if input.len() < 2 { return ValidationResult::invalid("Name must be at least 2 characters".to_string()); } if input.len() > 100 { return ValidationResult::invalid("Name is too long".to_string()); } if name_regex.is_match(input) { let normalized = input .split_whitespace() .map(|word| { let mut chars = word.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::() + chars.as_str(), } }) .collect::>() .join(" "); ValidationResult::valid(normalized) } else { ValidationResult::invalid(InputType::Name.error_message()) } } fn validate_integer(input: &str) -> ValidationResult { let cleaned = input .replace(",", "") .replace(".", "") .replace(" ", "") .trim() .to_string(); match cleaned.parse::() { Ok(num) => ValidationResult::valid_with_metadata( num.to_string(), serde_json::json!({ "value": num }), ), Err(_) => ValidationResult::invalid(InputType::Integer.error_message()), } } fn validate_float(input: &str) -> ValidationResult { let cleaned = input.replace(" ", "").replace(",", ".").trim().to_string(); match cleaned.parse::() { Ok(num) => ValidationResult::valid_with_metadata( format!("{:.2}", num), serde_json::json!({ "value": num }), ), Err(_) => ValidationResult::invalid(InputType::Float.error_message()), } } fn validate_boolean(input: &str) -> ValidationResult { let lower = input.to_lowercase(); let true_values = [ "yes", "y", "true", "1", "sim", "s", "si", "oui", "ja", "da", "ok", "yeah", "yep", "sure", "confirm", "confirmed", "accept", "agreed", "agree", ]; let false_values = [ "no", "n", "false", "0", "não", "nao", "non", "nein", "net", "nope", "cancel", "deny", "denied", "reject", "declined", "disagree", ]; if true_values.contains(&lower.as_str()) { ValidationResult::valid_with_metadata( "true".to_string(), serde_json::json!({ "value": true }), ) } else if false_values.contains(&lower.as_str()) { ValidationResult::valid_with_metadata( "false".to_string(), serde_json::json!({ "value": false }), ) } else { ValidationResult::invalid(InputType::Boolean.error_message()) } } 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(); let minute: u32 = caps[2].parse().unwrap(); return ValidationResult::valid_with_metadata( format!("{:02}:{:02}", hour, minute), serde_json::json!({ "hour": hour, "minute": minute }), ); } 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) { let mut hour: u32 = caps[1].parse().unwrap(); let minute: u32 = caps[2].parse().unwrap(); let period = caps[3].to_uppercase(); if period.starts_with('P') && hour != 12 { hour += 12; } else if period.starts_with('A') && hour == 12 { hour = 0; } return ValidationResult::valid_with_metadata( format!("{:02}:{:02}", hour, minute), serde_json::json!({ "hour": hour, "minute": minute }), ); } ValidationResult::invalid(InputType::Hour.error_message()) } fn validate_money(input: &str) -> ValidationResult { let cleaned = input .replace("R$", "") .replace("$", "") .replace("€", "") .replace("£", "") .replace("¥", "") .replace(" ", "") .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(",", ".") } else { cleaned.replace(",", "") } } else if cleaned.contains(',') { cleaned.replace(",", ".") } else { cleaned }; match normalized.parse::() { Ok(amount) if amount >= 0.0 => ValidationResult::valid_with_metadata( format!("{:.2}", amount), serde_json::json!({ "value": amount }), ), _ => ValidationResult::invalid(InputType::Money.error_message()), } } 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) }; ValidationResult::valid_with_metadata( formatted.clone(), serde_json::json!({ "digits": digits, "formatted": formatted }), ) } 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( formatted.clone(), serde_json::json!({ "digits": cleaned, "formatted": formatted, "country": "BR" }), ); } 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]) } else { cleaned.clone() }; return ValidationResult::valid_with_metadata( formatted.clone(), serde_json::json!({ "digits": cleaned, "formatted": formatted, "country": "US" }), ); } 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( cleaned.to_uppercase(), serde_json::json!({ "formatted": cleaned.to_uppercase(), "country": "UK" }), ); } ValidationResult::invalid(InputType::Zipcode.error_message()) } 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"), ("es", "spanish", "espanhol", "español"), ("fr", "french", "francês", "frances"), ("de", "german", "alemão", "alemao"), ("it", "italian", "italiano", ""), ("ja", "japanese", "japonês", "japones"), ("zh", "chinese", "chinês", "chines"), ("ko", "korean", "coreano", ""), ("ru", "russian", "russo", ""), ("ar", "arabic", "árabe", "arabe"), ("hi", "hindi", "", ""), ("nl", "dutch", "holandês", "holandes"), ("pl", "polish", "polonês", "polones"), ("tr", "turkish", "turco", ""), ]; for entry in &languages { let code = entry.0; let variants = [entry.1, entry.2, entry.3]; if lower.as_str() == code || variants .iter() .any(|v| !v.is_empty() && lower.as_str() == *v) { return ValidationResult::valid_with_metadata( code.to_string(), serde_json::json!({ "code": code, "input": input }), ); } } if lower.len() == 2 && lower.chars().all(|c| c.is_ascii_lowercase()) { return ValidationResult::valid(lower); } ValidationResult::invalid(InputType::Language.error_message()) } 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 sum1: u32 = digits_vec[0..9] .iter() .enumerate() .map(|(i, &d)| d * (10 - i as u32)) .sum(); let check1 = (sum1 * 10) % 11; let check1 = if check1 == 10 { 0 } else { check1 }; if check1 != digits_vec[9] { return ValidationResult::invalid("Invalid CPF".to_string()); } let sum2: u32 = digits_vec[0..10] .iter() .enumerate() .map(|(i, &d)| d * (11 - i as u32)) .sum(); let check2 = (sum2 * 10) % 11; let check2 = if check2 == 10 { 0 } else { check2 }; if check2 != digits_vec[10] { return ValidationResult::invalid("Invalid CPF".to_string()); } let formatted = format!( "{}.{}.{}-{}", &digits[0..3], &digits[3..6], &digits[6..9], &digits[9..11] ); ValidationResult::valid_with_metadata( formatted.clone(), serde_json::json!({ "digits": digits, "formatted": formatted }), ) } 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 weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let sum1: u32 = digits_vec[0..12] .iter() .zip(weights1.iter()) .map(|(&d, &w)| d * w) .sum(); let check1 = sum1 % 11; let check1 = if check1 < 2 { 0 } else { 11 - check1 }; if check1 != digits_vec[12] { 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() .zip(weights2.iter()) .map(|(&d, &w)| d * w) .sum(); let check2 = sum2 % 11; let check2 = if check2 < 2 { 0 } else { 11 - check2 }; if check2 != digits_vec[13] { return ValidationResult::invalid("Invalid CNPJ".to_string()); } let formatted = format!( "{}.{}.{}/{}-{}", &digits[0..2], &digits[2..5], &digits[5..8], &digits[8..12], &digits[12..14] ); ValidationResult::valid_with_metadata( formatted.clone(), serde_json::json!({ "digits": digits, "formatted": formatted }), ) } fn validate_url(input: &str) -> ValidationResult { let url_str = if !input.starts_with("http://") && !input.starts_with("https://") { format!("https://{}", input) } else { input.to_string() }; let url_regex = Regex::new( r"^https?://[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+(/[-a-zA-Z0-9()@:%_\+.~#?&/=]*)?$" ).unwrap(); if url_regex.is_match(&url_str) { ValidationResult::valid(url_str) } else { ValidationResult::invalid(InputType::Url.error_message()) } } fn validate_uuid(input: &str) -> ValidationResult { match Uuid::parse_str(input.trim()) { Ok(uuid) => ValidationResult::valid(uuid.to_string()), Err(_) => ValidationResult::invalid(InputType::Uuid.error_message()), } } fn validate_color(input: &str) -> ValidationResult { let lower = input.to_lowercase().trim().to_string(); let named_colors = [ ("red", "#FF0000"), ("green", "#00FF00"), ("blue", "#0000FF"), ("white", "#FFFFFF"), ("black", "#000000"), ("yellow", "#FFFF00"), ("orange", "#FFA500"), ("purple", "#800080"), ("pink", "#FFC0CB"), ("gray", "#808080"), ("grey", "#808080"), ("brown", "#A52A2A"), ("cyan", "#00FFFF"), ("magenta", "#FF00FF"), ]; for (name, hex) in &named_colors { if lower == *name { return ValidationResult::valid_with_metadata( hex.to_string(), serde_json::json!({ "name": name, "hex": hex }), ); } } 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(); let full_hex = if hex.len() == 3 { hex.chars() .map(|c| format!("{}{}", c, c)) .collect::() } else { hex }; 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) { let r: u8 = caps[1].parse().unwrap_or(0); let g: u8 = caps[2].parse().unwrap_or(0); let b: u8 = caps[3].parse().unwrap_or(0); return ValidationResult::valid(format!("#{:02X}{:02X}{:02X}", r, g, b)); } ValidationResult::invalid(InputType::Color.error_message()) } 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; for c in digits.chars().rev() { let mut digit = c.to_digit(10).unwrap(); if double { digit *= 2; if digit > 9 { digit -= 9; } } sum += digit; double = !double; } if sum % 10 != 0 { return ValidationResult::invalid("Invalid card number".to_string()); } let card_type = if digits.starts_with('4') { "Visa" } else if digits.starts_with("51") || digits.starts_with("52") || digits.starts_with("53") || digits.starts_with("54") || digits.starts_with("55") { "Mastercard" } else if digits.starts_with("34") || digits.starts_with("37") { "American Express" } else if digits.starts_with("36") || digits.starts_with("38") { "Diners Club" } else if digits.starts_with("6011") || digits.starts_with("65") { "Discover" } else { "Unknown" }; let masked = format!( "{} **** **** {}", &digits[0..4], &digits[digits.len() - 4..] ); ValidationResult::valid_with_metadata( masked.clone(), serde_json::json!({ "masked": masked, "last_four": &digits[digits.len()-4..], "card_type": card_type }), ) } fn validate_password(input: &str) -> ValidationResult { if input.len() < 8 { return ValidationResult::invalid("Password must be at least 8 characters".to_string()); } let has_upper = input.chars().any(|c| c.is_uppercase()); let has_lower = input.chars().any(|c| c.is_lowercase()); let has_digit = input.chars().any(|c| c.is_ascii_digit()); let has_special = input.chars().any(|c| !c.is_alphanumeric()); let strength = match (has_upper, has_lower, has_digit, has_special) { (true, true, true, true) => "strong", (true, true, true, false) | (true, true, false, true) | (true, false, true, true) => { "medium" } _ => "weak", }; ValidationResult::valid_with_metadata( "[PASSWORD SET]".to_string(), serde_json::json!({ "strength": strength, "length": input.len() }), ) } 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( opt.clone(), serde_json::json!({ "index": i, "value": opt }), ); } } if let Ok(num) = lower_input.parse::() { if num >= 1 && num <= options.len() { let selected = &options[num - 1]; return ValidationResult::valid_with_metadata( selected.clone(), serde_json::json!({ "index": num - 1, "value": selected }), ); } } let matches: Vec<&String> = options .iter() .filter(|opt| opt.to_lowercase().contains(&lower_input)) .collect(); if matches.len() == 1 { let idx = options.iter().position(|o| o == matches[0]).unwrap(); return ValidationResult::valid_with_metadata( matches[0].clone(), serde_json::json!({ "index": idx, "value": matches[0] }), ); } ValidationResult::invalid(format!("Please select one of: {}", options.join(", "))) } pub async fn execute_talk( state: Arc, user_session: UserSession, message: String, ) -> 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); let suggestions_json: Result, _> = redis::cmd("LRANGE") .arg(redis_key.as_str()) .arg(0) .arg(-1) .query_async(&mut conn) .await; if let Ok(suggestions_list) = suggestions_json { suggestions = suggestions_list .into_iter() .filter_map(|s| serde_json::from_str(&s).ok()) .collect(); } } } let response = BotResponse { bot_id: user_session.bot_id.to_string(), user_id: user_session.user_id.to_string(), session_id: user_session.id.to_string(), channel: "web".to_string(), content: message, message_type: MessageType::USER, stream_token: None, is_complete: true, suggestions, context_name: None, context_length: 0, context_max_length: 0, }; 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 .send_message_to_session(&user_id, response_clone) .await { error!("Failed to send TALK message via web adapter: {}", e); } else { trace!("TALK message sent via web adapter"); } }); Ok(response) } pub fn talk_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); engine .register_custom_syntax(&["TALK", "$expr$"], true, move |context, inputs| { let message = context.eval_expression_tree(&inputs[0])?.to_string(); let state_for_talk = Arc::clone(&state_clone); let user_for_talk = user_clone.clone(); tokio::spawn(async move { if let Err(e) = execute_talk(state_for_talk, user_for_talk, message).await { error!("Error executing TALK command: {}", e); } }); Ok(Dynamic::UNIT) }) .unwrap(); } pub async fn process_hear_input( state: &AppState, session_id: Uuid, variable_name: &str, 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 data: Result = redis::cmd("GET").arg(&key).query_async(&mut conn).await; match data { Ok(json_str) => serde_json::from_str::(&json_str).ok(), Err(_) => None, } } else { None } } else { None }; let input_type = wait_data .as_ref() .and_then(|d| d.get("type")) .and_then(|t| t.as_str()) .unwrap_or("any"); let options = wait_data .as_ref() .and_then(|d| d.get("options")) .and_then(|o| o.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .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 { if let Some(img) = atts .iter() .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(( img.url.clone(), Some(serde_json::json!({ "attachment": img })), )); } } return Err(validation_type.error_message()); } InputType::Audio => { if let Some(atts) = &attachments { if let Some(audio) = atts .iter() .find(|a| a.mime_type.as_deref().unwrap_or("").starts_with("audio/")) { return process_audio_to_text(state, &audio.url).await; } } return Err(validation_type.error_message()); } InputType::Video => { if let Some(atts) = &attachments { if let Some(video) = atts .iter() .find(|a| a.mime_type.as_deref().unwrap_or("").starts_with("video/")) { return process_video_description(state, &video.url).await; } } return Err(validation_type.error_message()); } InputType::File | InputType::Document => { if let Some(atts) = &attachments { if let Some(doc) = atts.first() { return Ok(( doc.url.clone(), Some(serde_json::json!({ "attachment": doc })), )); } } return Err(validation_type.error_message()); } _ => {} } 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 _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; } } Ok((result.normalized_value, result.metadata)) } else { Err(result .error_message .unwrap_or_else(|| validation_type.error_message())) } } 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::*; use diesel::prelude::*; bot_memories .filter(key.eq("botmodels-url")) .select(value) .first::(&mut conn) .ok() }); config_url.unwrap_or_else(|| { std::env::var("BOTMODELS_URL").unwrap_or_else(|_| "http://localhost:8001".to_string()) }) }; let client = reqwest::Client::new(); let image_data = client .get(image_url) .send() .await .map_err(|e| format!("Failed to download image: {}", e))? .bytes() .await .map_err(|e| format!("Failed to read image: {}", e))?; let response = client .post(format!("{}/api/v1/vision/qrcode", botmodels_url)) .header("Content-Type", "application/octet-stream") .body(image_data.to_vec()) .send() .await .map_err(|e| format!("Failed to call botmodels: {}", e))?; if response.status().is_success() { let result: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; if let Some(qr_data) = result.get("data").and_then(|d| d.as_str()) { Ok(( qr_data.to_string(), Some(serde_json::json!({ "type": "qrcode", "raw": result })), )) } else { Err("No QR code found in image".to_string()) } } else { Err("Failed to read QR code".to_string()) } } async fn process_audio_to_text( _state: &AppState, audio_url: &str, ) -> Result<(String, Option), String> { let botmodels_url = std::env::var("BOTMODELS_URL").unwrap_or_else(|_| "http://localhost:8001".to_string()); let client = reqwest::Client::new(); let audio_data = client .get(audio_url) .send() .await .map_err(|e| format!("Failed to download audio: {}", e))? .bytes() .await .map_err(|e| format!("Failed to read audio: {}", e))?; let response = client .post(format!("{}/api/v1/speech/to-text", botmodels_url)) .header("Content-Type", "application/octet-stream") .body(audio_data.to_vec()) .send() .await .map_err(|e| format!("Failed to call botmodels: {}", e))?; if response.status().is_success() { let result: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; if let Some(text) = result.get("text").and_then(|t| t.as_str()) { Ok(( text.to_string(), Some(serde_json::json!({ "type": "audio_transcription", "language": result.get("language"), "confidence": result.get("confidence") })), )) } else { Err("Could not transcribe audio".to_string()) } } else { Err("Failed to process audio".to_string()) } } async fn process_video_description( _state: &AppState, video_url: &str, ) -> Result<(String, Option), String> { let botmodels_url = std::env::var("BOTMODELS_URL").unwrap_or_else(|_| "http://localhost:8001".to_string()); let client = reqwest::Client::new(); let video_data = client .get(video_url) .send() .await .map_err(|e| format!("Failed to download video: {}", e))? .bytes() .await .map_err(|e| format!("Failed to read video: {}", e))?; let response = client .post(format!("{}/api/v1/vision/describe-video", botmodels_url)) .header("Content-Type", "application/octet-stream") .body(video_data.to_vec()) .send() .await .map_err(|e| format!("Failed to call botmodels: {}", e))?; if response.status().is_success() { let result: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; if let Some(description) = result.get("description").and_then(|d| d.as_str()) { Ok(( description.to_string(), Some(serde_json::json!({ "type": "video_description", "frame_count": result.get("frame_count"), "url": video_url })), )) } else { Err("Could not describe video".to_string()) } } else { Err("Failed to process video".to_string()) } }