1547 lines
47 KiB
Rust
1547 lines
47 KiB
Rust
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<String>),
|
|
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<String>,
|
|
pub metadata: Option<serde_json::Value>,
|
|
}
|
|
|
|
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<AppState>, 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<AppState>, 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<AppState>, 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<AppState>, 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<String> = 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::<String>() + chars.as_str(),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.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::<i64>() {
|
|
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::<f64>() {
|
|
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::<f64>() {
|
|
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<u32> = 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<u32> = 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::<String>()
|
|
} 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::<usize>() {
|
|
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<AppState>,
|
|
user_session: UserSession,
|
|
message: String,
|
|
) -> Result<BotResponse, Box<dyn std::error::Error + Send + Sync>> {
|
|
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<Vec<String>, _> = 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<AppState>, 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<Vec<crate::shared::models::Attachment>>,
|
|
) -> Result<(String, Option<serde_json::Value>), 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<String, _> = redis::cmd("GET").arg(&key).query_async(&mut conn).await;
|
|
|
|
match data {
|
|
Ok(json_str) => serde_json::from_str::<serde_json::Value>(&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::<Vec<_>>()
|
|
});
|
|
|
|
|
|
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<serde_json::Value>), 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::<String>(&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<serde_json::Value>), 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<serde_json::Value>), 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())
|
|
}
|
|
}
|