diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6c2b69a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'gbserver'", + "cargo": { + "args": [ + "build", + "--bin=gbserver", + "--package=gbserver" + ], + "filter": { + "name": "gbserver", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'gbserver'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=gbserver", + "--package=gbserver" + ], + "filter": { + "name": "gbserver", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0ac5b96..d906a7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,6 +409,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -1750,6 +1756,7 @@ dependencies = [ "mailparse", "minio", "native-tls", + "num-format", "regex", "reqwest 0.11.27", "rhai", @@ -2599,7 +2606,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags 1.3.2", "cfg-if", "ryu", @@ -2994,6 +3001,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec 0.7.6", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" diff --git a/Cargo.toml b/Cargo.toml index 47d7cf2..46ba11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,3 +60,4 @@ urlencoding = "2.1" regex = "1.10" uuid = { version = "1.4", features = ["serde", "v4"] } # v4, v7, etc. as needed zip = "4.3.0" +num-format = "0.4" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a80123d..2037f17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,7 +47,7 @@ async fn main() -> std::io::Result<()> { ensure_llama_servers_running() .await .expect("Failed to initialize LLM local server."); - + initialize_browser_pool() .await .expect("Failed to initialize browser pool"); diff --git a/src/services/automation.rs b/src/services/automation.rs index 8eee1ee..08206d0 100644 --- a/src/services/automation.rs +++ b/src/services/automation.rs @@ -52,7 +52,7 @@ impl AutomationService { sqlx::query_as::<_, Automation>( r#" SELECT id, kind, target, schedule, param, is_active, last_triggered - FROM system_automations + FROM public.system_automations WHERE is_active = true "#, ) @@ -122,7 +122,7 @@ impl AutomationService { async fn update_last_triggered(&self, automation_id: Uuid) { if let Some(pool) = &self.state.db { if let Err(e) = sqlx::query!( - "UPDATE system_automations SET last_triggered = $1 WHERE id = $2", + "UPDATE public.system_automations SET last_triggered = $1 WHERE id = $2", Utc::now(), automation_id ) diff --git a/src/services/email.rs b/src/services/email.rs index ff87a67..af0d556 100644 --- a/src/services/email.rs +++ b/src/services/email.rs @@ -497,7 +497,7 @@ pub async fn save_click( state: web::Data, ) -> HttpResponse { let (campaign_id, email) = path.into_inner(); - let _ = sqlx::query("INSERT INTO clicks (campaign_id, email, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (campaign_id, email) DO UPDATE SET updated_at = NOW()") + let _ = sqlx::query("INSERT INTO public.clicks (campaign_id, email, updated_at) VALUES ($1, $2, NOW()) ON CONFLICT (campaign_id, email) DO UPDATE SET updated_at = NOW()") .bind(campaign_id) .bind(email) .execute(state.db.as_ref().unwrap()) diff --git a/src/services/keywords/format.rs b/src/services/keywords/format.rs new file mode 100644 index 0000000..d085dd8 --- /dev/null +++ b/src/services/keywords/format.rs @@ -0,0 +1,193 @@ +use rhai::{Dynamic, Engine}; +use chrono::{NaiveDateTime, Timelike, Datelike}; +use num_format::{Locale, ToFormattedString}; +use std::str::FromStr; + +pub fn format_keyword(engine: &mut Engine) { + engine + .register_custom_syntax(&["FORMAT", "$expr$", "$expr$"], false, { + move |context, inputs| { + let value_dyn = context.eval_expression_tree(&inputs[0])?; + let pattern_dyn = context.eval_expression_tree(&inputs[1])?; + + let value_str = value_dyn.to_string(); + let pattern = pattern_dyn.to_string(); + + // --- NUMÉRICO --- + if let Ok(num) = f64::from_str(&value_str) { + let formatted = if pattern.starts_with("N") || pattern.starts_with("C") { + // extrai partes: prefixo, casas decimais, locale + let (prefix, decimals, locale_tag) = parse_pattern(&pattern); + + let locale = get_locale(&locale_tag); + let symbol = if prefix == "C" { + get_currency_symbol(&locale_tag) + } else { + "" + }; + + let int_part = num.trunc() as i64; + let frac_part = num.fract(); + + if decimals == 0 { + format!("{}{}", symbol, int_part.to_formatted_string(&locale)) + } else { + let frac_scaled = + ((frac_part * 10f64.powi(decimals as i32)).round()) as i64; + format!( + "{}{}.{:0width$}", + symbol, + int_part.to_formatted_string(&locale), + frac_scaled, + width = decimals + ) + } + } else { + match pattern.as_str() { + "n" => format!("{:.2}", num), + "F" => format!("{:.2}", num), + "f" => format!("{}", num), + "0%" => format!("{:.0}%", num * 100.0), + _ => format!("{}", num), + } + }; + + return Ok(Dynamic::from(formatted)); + } + + // --- DATA --- + if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") { + let formatted = apply_date_format(&dt, &pattern); + return Ok(Dynamic::from(formatted)); + } + + // --- TEXTO --- + let formatted = apply_text_placeholders(&value_str, &pattern); + Ok(Dynamic::from(formatted)) + } + }) + .unwrap(); +} + +// ====================== +// Extração de locale + precisão +// ====================== +fn parse_pattern(pattern: &str) -> (String, usize, String) { + let mut prefix = String::new(); + let mut decimals: usize = 2; // padrão 2 casas + let mut locale_tag = "en".to_string(); + + // ex: "C2[pt]" ou "N3[fr]" + if pattern.starts_with('C') { + prefix = "C".to_string(); + } else if pattern.starts_with('N') { + prefix = "N".to_string(); + } + + // procura número após prefixo + let rest = &pattern[1..]; + let mut num_part = String::new(); + for ch in rest.chars() { + if ch.is_ascii_digit() { + num_part.push(ch); + } else { + break; + } + } + if !num_part.is_empty() { + decimals = num_part.parse().unwrap_or(2); + } + + // procura locale entre colchetes + if let Some(start) = pattern.find('[') { + if let Some(end) = pattern.find(']') { + if end > start { + locale_tag = pattern[start + 1..end].to_string(); + } + } + } + + (prefix, decimals, locale_tag) +} + +fn get_locale(tag: &str) -> Locale { + match tag { + "en" => Locale::en, + "fr" => Locale::fr, + "de" => Locale::de, + "pt" => Locale::pt, + "it" => Locale::it, + "es" => Locale::es, + _ => Locale::en, + } +} + +fn get_currency_symbol(tag: &str) -> &'static str { + match tag { + "en" => "$", + "pt" => "R$ ", + "fr" | "de" | "es" | "it" => "€", + _ => "$", + } +} + +// ================== +// SUPORTE A DATAS +// ================== +fn apply_date_format(dt: &NaiveDateTime, pattern: &str) -> String { + let mut output = pattern.to_string(); + + let year = dt.year(); + let month = dt.month(); + let day = dt.day(); + let hour24 = dt.hour(); + let minute = dt.minute(); + let second = dt.second(); + let millis = dt.and_utc().timestamp_subsec_millis(); + + output = output.replace("yyyy", &format!("{:04}", year)); + output = output.replace("yy", &format!("{:02}", year % 100)); + output = output.replace("MM", &format!("{:02}", month)); + output = output.replace("M", &format!("{}", month)); + output = output.replace("dd", &format!("{:02}", day)); + output = output.replace("d", &format!("{}", day)); + + output = output.replace("HH", &format!("{:02}", hour24)); + output = output.replace("H", &format!("{}", hour24)); + + let mut hour12 = hour24 % 12; + if hour12 == 0 { hour12 = 12; } + output = output.replace("hh", &format!("{:02}", hour12)); + output = output.replace("h", &format!("{}", hour12)); + + output = output.replace("mm", &format!("{:02}", minute)); + output = output.replace("m", &format!("{}", minute)); + + output = output.replace("ss", &format!("{:02}", second)); + output = output.replace("s", &format!("{}", second)); + + output = output.replace("fff", &format!("{:03}", millis)); + + output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" }); + output = output.replace("t", if hour24 < 12 { "A" } else { "P" }); + + output +} + +// ================== +// SUPORTE A TEXTO +// ================== +fn apply_text_placeholders(value: &str, pattern: &str) -> String { + let mut result = String::new(); + + for ch in pattern.chars() { + match ch { + '@' => result.push_str(value), + '&' | '<' => result.push_str(&value.to_lowercase()), + '>' | '!' => result.push_str(&value.to_uppercase()), + _ => result.push(ch), // copia qualquer caractere literal + } + } + + result +} diff --git a/src/services/keywords/last.rs b/src/services/keywords/last.rs new file mode 100644 index 0000000..e806c07 --- /dev/null +++ b/src/services/keywords/last.rs @@ -0,0 +1,22 @@ +use rhai::Dynamic; +use rhai::Engine; + +pub fn last_keyword(engine: &mut Engine) { + engine + .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(); + + // Extrai a última palavra dividindo por espaço + let last_word = input_str + .split_whitespace() + .last() + .unwrap_or("") + .to_string(); + + Ok(Dynamic::from(last_word)) + } + }) + .unwrap(); +} diff --git a/src/services/keywords/mod.rs b/src/services/keywords/mod.rs index a85a9f1..5155810 100644 --- a/src/services/keywords/mod.rs +++ b/src/services/keywords/mod.rs @@ -2,6 +2,8 @@ pub mod create_draft; pub mod create_site; pub mod find; pub mod first; +pub mod last; +pub mod format; pub mod for_next; pub mod get; pub mod get_website; diff --git a/src/services/llm_local.rs b/src/services/llm_local.rs index 00ff628..5cebd8d 100644 --- a/src/services/llm_local.rs +++ b/src/services/llm_local.rs @@ -3,7 +3,7 @@ use dotenv::dotenv; use log::{error, info}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::env; +use std::{env, process::Command}; use tokio::time::{sleep, Duration}; // OpenAI-compatible request/response structures @@ -188,15 +188,26 @@ async fn start_llm_server( std::env::set_var("OMP_PLACES", "cores"); std::env::set_var("OMP_PROC_BIND", "close"); + let mut cmd = Command::new("sh"); // "cd {} && numactl --interleave=all ./llama-server -m {} --host 0.0.0.0 --port {} --threads 20 --threads-batch 40 --temp 0.7 --parallel 1 --repeat-penalty 1.1 --ctx-size 8192 --batch-size 8192 -n 4096 --mlock --no-mmap --flash-attn --no-kv-offload --no-mmap &", - let mut cmd = tokio::process::Command::new("sh"); - cmd.arg("-c").arg(format!( - "cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99 &", - llama_cpp_path, model_path, port - )); + #[cfg(target_os = "linux")] + { + cmd.arg("-c").arg(format!( + "cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99 &", + llama_cpp_path, model_path, port + )); + + } + + #[cfg(target_os = "windows")] + { + cmd.arg("/C").arg(format!( + "cd {} && llama-server.exe -m {} --host 0.0.0.0 --port {} --n-gpu-layers 99", + llama_cpp_path, model_path, port + )); + } - cmd.spawn()?; Ok(()) } @@ -206,13 +217,25 @@ async fn start_embedding_server( url: String, ) -> Result<(), Box> { let port = url.split(':').last().unwrap_or("8082"); + let mut cmd = Command::new("cmd"); - let mut cmd = tokio::process::Command::new("sh"); - cmd.arg("-c").arg(format!( - "cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &", - llama_cpp_path, model_path, port - )); + #[cfg(target_os = "windows")] + { + cmd.arg("/C").arg(format!( + "cd {} && llama-server.exe -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99", + llama_cpp_path, model_path, port + )); + + } + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + cmd.arg("-c").arg(format!( + "cd {} && ./llama-server -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99 &", + llama_cpp_path, model_path, port + )); + } + cmd.spawn()?; Ok(()) } @@ -431,7 +454,8 @@ struct LlamaCppEmbeddingRequest { // FIXED: Handle the stupid nested array format #[derive(Debug, Deserialize)] struct LlamaCppEmbeddingResponseItem { - pub index: usize, + #[serde(rename = "index")] + pub _index: usize, pub embedding: Vec>, // This is the fucked up part - embedding is an array of arrays } diff --git a/src/services/script.rs b/src/services/script.rs index ed7f8f0..a38a879 100644 --- a/src/services/script.rs +++ b/src/services/script.rs @@ -2,6 +2,8 @@ use crate::services::keywords::create_draft::create_draft_keyword; use crate::services::keywords::create_site::create_site_keyword; use crate::services::keywords::find::find_keyword; use crate::services::keywords::first::first_keyword; +use crate::services::keywords::last::last_keyword; +use crate::services::keywords::format::format_keyword; use crate::services::keywords::for_next::for_keyword; use crate::services::keywords::get::get_keyword; use crate::services::keywords::get_website::get_website_keyword; @@ -32,6 +34,8 @@ impl ScriptService { find_keyword(state, &mut engine); for_keyword(state, &mut engine); first_keyword(&mut engine); + last_keyword(&mut engine); + format_keyword(&mut engine); llm_keyword(state, &mut engine); get_website_keyword(state, &mut engine); get_keyword(state, &mut engine); diff --git a/src/services/web_automation.rs b/src/services/web_automation.rs index a48d0e8..35bb8a2 100644 --- a/src/services/web_automation.rs +++ b/src/services/web_automation.rs @@ -10,6 +10,7 @@ use std::future::Future; use std::pin::Pin; use std::process::Command; use std::sync::Arc; +use std::path::PathBuf; use thirtyfour::{DesiredCapabilities, WebDriver}; use tokio::fs; use tokio::sync::Semaphore; @@ -75,26 +76,32 @@ impl BrowserSetup { }) } - async fn find_brave() -> Result> { - let possible_paths = vec![ - // Windows - String::from(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"), - // macOS - String::from("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"), - // Linux - String::from("/usr/bin/brave-browser"), - String::from("/usr/bin/brave"), - ]; + async fn find_brave() -> Result> { + let mut possible_paths = vec![ + // Windows - Program Files + String::from(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"), + // macOS + String::from("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"), + // Linux + String::from("/usr/bin/brave-browser"), + String::from("/usr/bin/brave"), + ]; - for path in possible_paths { - if fs::metadata(&path).await.is_ok() { - return Ok(path); - } - } - - Err("Brave browser not found. Please install Brave first.".into()) + // Windows - AppData (usuário atual) + if let Ok(local_appdata) = env::var("LOCALAPPDATA") { + let mut path = PathBuf::from(local_appdata); + path.push("BraveSoftware\\Brave-Browser\\Application\\brave.exe"); + possible_paths.push(path.to_string_lossy().to_string()); } - async fn setup_chromedriver() -> Result> { + + for path in possible_paths { + if fs::metadata(&path).await.is_ok() { + return Ok(path); + } + } + + Err("Brave browser not found. Please install Brave first.".into()) +} async fn setup_chromedriver() -> Result> { // Create chromedriver directory in executable's parent directory let mut chromedriver_dir = env::current_exe()?.parent().unwrap().to_path_buf(); chromedriver_dir.push("chromedriver");