diff --git a/Cargo.lock b/Cargo.lock index 6ffd741..ecc1f21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,6 +1252,19 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downloader" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34" +dependencies = [ + "futures", + "rand 0.8.5", + "reqwest 0.12.20", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -1418,6 +1431,28 @@ dependencies = [ "regex", ] +[[package]] +name = "fantoccini" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64" +dependencies = [ + "base64 0.13.1", + "cookie", + "futures-core", + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "hyper-rustls 0.23.2", + "mime", + "serde", + "serde_json", + "time", + "tokio", + "url", + "webdriver", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1451,7 +1486,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.8", ] [[package]] @@ -1641,6 +1676,7 @@ dependencies = [ "bytes", "chrono", "dotenv", + "downloader", "env_logger 0.10.2", "futures", "futures-util", @@ -1651,17 +1687,21 @@ dependencies = [ "mailparse", "minio", "native-tls", + "regex", "reqwest 0.11.27", "rhai", + "scraper 0.18.1", "serde", "serde_json", "smartstring", "sqlx", "tempfile", + "thirtyfour", "tokio", "tokio-stream", "tracing", "tracing-subscriber", + "urlencoding", ] [[package]] @@ -2018,6 +2058,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.20.9", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.23.4", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -2028,10 +2083,10 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.28", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", ] @@ -2409,7 +2464,7 @@ dependencies = [ "regex", "reqwest 0.12.20", "reqwest-eventsource", - "scraper", + "scraper 0.20.0", "secrecy", "serde", "serde_json", @@ -2435,7 +2490,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -3240,7 +3295,7 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.1", - "ring", + "ring 0.17.14", "rustc-hash 2.1.1", "rustls 0.23.28", "rustls-pki-types", @@ -3461,7 +3516,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", "js-sys", @@ -3473,7 +3528,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.28", - "rustls-native-certs", + "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", "serde_json", @@ -3481,7 +3536,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower", "tower-http", @@ -3537,6 +3592,21 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -3547,7 +3617,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -3611,13 +3681,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ - "ring", + "ring 0.17.14", "rustls-webpki 0.101.7", "sct", ] @@ -3629,13 +3711,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", "rustls-webpki 0.103.3", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -3673,8 +3767,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.14", + "untrusted 0.9.0", ] [[package]] @@ -3683,9 +3777,9 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -3715,6 +3809,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scraper" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever 0.26.0", + "once_cell", + "selectors", + "tendril", +] + [[package]] name = "scraper" version = "0.20.0" @@ -3737,8 +3847,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.14", + "untrusted 0.9.0", ] [[package]] @@ -3838,6 +3948,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", @@ -3853,6 +3964,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3991,6 +4113,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -4254,6 +4382,15 @@ dependencies = [ "quote", ] +[[package]] +name = "stringmatch" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c0faab770316c3838f895fc2dfc3a8707ef4da48676f1014e1061ebd583b40" +dependencies = [ + "regex", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -4447,6 +4584,31 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" +[[package]] +name = "thirtyfour" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60aded5a858cc767f950549a9c0eecd61cb4648ad2c7f0ada5cbd7f441cb6ac3" +dependencies = [ + "async-trait", + "base64 0.13.1", + "chrono", + "cookie", + "fantoccini", + "futures", + "http 0.2.12", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_repr", + "stringmatch", + "thiserror 1.0.69", + "tokio", + "url", + "urlparse", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4615,6 +4777,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -4821,6 +4994,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -4844,6 +5023,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "urlparse" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" + [[package]] name = "utf-8" version = "0.7.6" @@ -5026,6 +5211,35 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webdriver" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" +dependencies = [ + "base64 0.13.1", + "bytes", + "cookie", + "http 0.2.12", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "unicode-segmentation", + "url", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/Cargo.toml b/Cargo.toml index fdf68e6..1b24bea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ actix-cors = "0.6" actix-multipart = "0.6" actix-web = "4" actix-ws="0.3.0" +thirtyfour = { version = "0.30" } +downloader = "0.2.8" anyhow = "1.0" async-stream = "0.3" bytes = "1.1" @@ -38,3 +40,6 @@ tokio = { version = "1", features = ["full"] } tokio-stream = "0.1.17" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt"] } +scraper = "0.18" +urlencoding = "2.1" +regex = "1.10" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8ef871f..8a875d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use actix_cors::Cors; use actix_web::http::header; use actix_web::{web, App, HttpServer}; @@ -10,6 +12,8 @@ use services::llm::*; use services::script::*; use services::state::*; use sqlx::PgPool; + +use crate::services::web_automation::BrowserPool; //use services:: find::*; mod services; @@ -29,11 +33,17 @@ async fn main() -> std::io::Result<()> { .await .expect("Failed to initialize Minio"); + let browser_pool = Arc::new(BrowserPool::new( + "http://localhost:9515".to_string(), + 5, + "/usr/bin/brave-browser-beta".to_string(), + )); let app_state = web::Data::new(AppState { db: db.into(), db_custom: db_custom.into(), config: Some(config.clone()), minio_client: minio_client.into(), + browser_pool: browser_pool.clone(), }); let script_service = ScriptService::new(&app_state.clone()); diff --git a/src/prompts/business/data-enrichment.bas b/src/prompts/business/data-enrichment.bas index d4bfb62..a3de98f 100644 --- a/src/prompts/business/data-enrichment.bas +++ b/src/prompts/business/data-enrichment.bas @@ -1,5 +1,21 @@ -let items = FIND "gb.rob", "ACTION=EMUL1" +let items = FIND "gb.rob", "ACTION=EMUL" FOR EACH item IN items - let text = GET "https://pragmatismo.com.br" PRINT item.company + let website = GET WEBSITE item.company "website" + PRINT website + WAIT 10 + let page = GET website + + let prompt = "Create a website for " + item.company + " with the following details: " + page + + let alias = REWRITE "Return a single word for {item.company} like a token, no spaces, no special characters, no numbers, no uppercase letters." + + CREATE SITE item.company + "bot", website, "site", prompt + + let to = item.emailcto + let subject = "Simulador criado " + item.company + let body = "O simulador " + item.company + " foi criado com sucesso. Acesse o site: " + item.company + "bot" + + CREATE DRAFT to, subject, body + NEXT item \ No newline at end of file diff --git a/src/services.rs b/src/services.rs index 79d2957..5077993 100644 --- a/src/services.rs +++ b/src/services.rs @@ -1,8 +1,9 @@ pub mod config; -pub mod utils; -pub mod state; pub mod email; -pub mod keywords; pub mod file; +pub mod keywords; pub mod llm; -pub mod script; \ No newline at end of file +pub mod script; +pub mod state; +pub mod utils; +pub mod web_automation; diff --git a/src/services/config.rs b/src/services/config.rs index e6c8b58..0e9e8a8 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -6,7 +6,6 @@ pub struct AppConfig { pub server: ServerConfig, pub database: DatabaseConfig, pub database_custom: DatabaseConfig, - pub email: EmailConfig, pub ai: AIConfig, } diff --git a/src/services/email.rs b/src/services/email.rs index 520a1dc..4918359 100644 --- a/src/services/email.rs +++ b/src/services/email.rs @@ -160,102 +160,211 @@ fn parse_from_field(from: &str) -> (String, String) { ("Unknown".to_string(), from.to_string()) } -// #[actix_web::post("/emails/suggest-answer/{email_id}")] -// pub async fn suggest_answer( -// path: web::Path, -// state: web::Data, -// ) -> Result { -// let email_id = path.into_inner(); -// let config = state -// .config -// .as_ref() -// .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; -// // let mut session = create_imap_session(&config.email).await?; +#[derive(serde::Deserialize)] +pub struct SaveDraftRequest { + pub to: String, + pub subject: String, + pub cc: Option, + pub text: String, +} -// // session -// // .select("INBOX") -// // .await -// // .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?; +#[derive(serde::Serialize)] +pub struct SaveDraftResponse { + pub success: bool, + pub message: String, + pub draft_id: Option, +} -// // let messages = session -// // .fetch(&email_id, "RFC822.HEADER BODY[TEXT]") -// // .await -// // .map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?; +#[derive(serde::Deserialize)] +pub struct GetLatestEmailRequest { + pub from_email: String, +} -// // let msg = messages -// // .iter() -// // .next() -// // .ok_or_else(|| actix_web::error::ErrorNotFound("Email not found"))?; +#[derive(serde::Serialize)] +pub struct LatestEmailResponse { + pub success: bool, + pub email_text: Option, + pub message: String, +} -// // let header = msg -// // .header() -// // .ok_or_else(|| ErrorInternalServerError("No header found"))?; +#[actix_web::post("/emails/save_draft")] +pub async fn save_draft( + state: web::Data, + draft_data: web::Json, +) -> Result, actix_web::Error> { + let config = state + .config + .as_ref() + .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; -// // let body = msg -// // .text() -// // .ok_or_else(|| ErrorInternalServerError("No body found"))?; + match save_email_draft(&config.email, &draft_data).await { + Ok(draft_id) => Ok(web::Json(SaveDraftResponse { + success: true, + message: "Draft saved successfully".to_string(), + draft_id: Some(draft_id), + })), + Err(e) => Ok(web::Json(SaveDraftResponse { + success: false, + message: format!("Failed to save draft: {}", e), + draft_id: None, + })) + } +} -// // let header_str = String::from_utf8_lossy(header); -// // let mut subject = String::new(); -// // let mut from_info = String::new(); -// // for line in header_str.lines() { -// // if line.starts_with("Subject: ") { -// // subject = line.strip_prefix("Subject: ").unwrap_or("").to_string(); -// // } else if line.starts_with("From: ") { -// // from_info = line.strip_prefix("From: ").unwrap_or("").to_string(); -// // } -// // } +pub async fn save_email_draft( + email_config: &EmailConfig, + draft_data: &SaveDraftRequest, +) -> Result> { + // Establish connection + let tls = native_tls::TlsConnector::builder().build()?; + let client = imap::connect( + (email_config.server.as_str(), 993), + email_config.server.as_str(), + &tls, + )?; -// // let body_text = String::from_utf8_lossy(body); + // Login + let mut session = client.login(&email_config.username, &email_config.password) + .map_err(|e| format!("Login failed: {:?}", e))?; -// // let response = serde_json::json!({ -// // "suggested_response": "Thank you for your email. I will review this and get back to you shortly.", -// // "prompt": format!( -// // "Email from: {}\nSubject: {}\n\nBody:\n{}\n\n---\n\nPlease draft a professional response to this email.", -// // from_info, subject, body_text.lines().take(20).collect::>().join("\n") -// // ) -// // }); + // Select or create Drafts folder + if session.select("Drafts").is_err() { + // Try to create Drafts folder if it doesn't exist + session.create("Drafts")?; + session.select("Drafts")?; + } -// // session.logout().await.ok(); -// //Ok(HttpResponse::Ok().json(response)) -// } + // Create email message + let cc_header = draft_data.cc.as_deref() + .filter(|cc| !cc.is_empty()) + .map(|cc| format!("Cc: {}\r\n", cc)) + .unwrap_or_default(); -// #[actix_web::post("/emails/archive/{email_id}")] -// pub async fn archive_email( -// path: web::Path, -// state: web::Data, -// ) -> Result { -// let email_id = path.into_inner(); -// let config = state -// .config -// .as_ref() -// .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; + let email_message = format!( + "From: {}\r\nTo: {}\r\n{}Subject: {}\r\nDate: {}\r\n\r\n{}", + email_config.username, + draft_data.to, + cc_header, + draft_data.subject, + chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"), + draft_data.text + ); -// let mut session = create_imap_session(&config.email).await?; + // Append to Drafts folder + session.append("Drafts", &email_message)?; + + session.logout()?; -// session -// .select("INBOX") -// .await -// .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?; + Ok(chrono::Utc::now().timestamp().to_string()) +} -// // Create Archive folder if it doesn't exist -// session.create("Archive").await.ok(); // Ignore error if folder exists +#[actix_web::post("/emails/get_latest_from")] +pub async fn get_latest_email_from( + state: web::Data, + request: web::Json, +) -> Result, actix_web::Error> { + let config = state + .config + .as_ref() + .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; -// // Move email to Archive folder -// session.mv(&email_id, "Archive").await.map_err(|e| { -// ErrorInternalServerError(format!("Failed to move email to archive: {:?}", e)) -// })?; + match fetch_latest_email_from_sender(&config.email, &request.from_email).await { + Ok(email_text) => Ok(web::Json(LatestEmailResponse { + success: true, + email_text: Some(email_text), + message: "Latest email retrieved successfully".to_string(), + })), + Err(e) => { + if e.to_string().contains("No emails found") { + Ok(web::Json(LatestEmailResponse { + success: false, + email_text: None, + message: e.to_string(), + })) + } else { + Err(ErrorInternalServerError(e)) + } + } + } +} +pub async fn fetch_latest_email_from_sender( + email_config: &EmailConfig, + from_email: &str, +) -> Result> { + // Establish connection + let tls = native_tls::TlsConnector::builder().build()?; + let client = imap::connect( + (email_config.server.as_str(), 993), + email_config.server.as_str(), + &tls, + )?; -// session.logout().await.ok(); + // Login + let mut session = client.login(&email_config.username, &email_config.password) + .map_err(|e| format!("Login failed: {:?}", e))?; + + // Try to select Archive folder first, then fall back to INBOX + if session.select("Archive").is_err() { + session.select("INBOX")?; + } + + // Search for emails from the specified sender + let search_query = format!("FROM \"{}\"", from_email); + let messages = session.search(&search_query)?; + + if messages.is_empty() { + session.logout()?; + return Err(format!("No emails found from {}", from_email).into()); + } + + // Get the latest message (highest sequence number) + let latest_seq = messages.iter().max().unwrap(); + + // Fetch the entire message + let messages = session.fetch(latest_seq.to_string(), "RFC822")?; + + let mut email_text = String::new(); + + for msg in messages.iter() { + let body = msg.body().ok_or("No body found in email")?; + + // Parse the complete email message + let parsed = parse_mail(body)?; + + // Extract headers + let headers = parsed.get_headers(); + let subject = headers.get_first_value("Subject").unwrap_or_default(); + let from = headers.get_first_value("From").unwrap_or_default(); + let date = headers.get_first_value("Date").unwrap_or_default(); + let to = headers.get_first_value("To").unwrap_or_default(); + + // Extract body text + let body_text = if let Some(body_part) = parsed.subparts.iter().find(|p| p.ctype.mimetype == "text/plain") { + body_part.get_body().unwrap_or_default() + } else { + parsed.get_body().unwrap_or_default() + }; + + // Format the email text ready for reply with headers + email_text = format!( + "--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n", + from, to, date, subject, body_text + ); + + break; // We only want the first (and should be only) message + } + + session.logout()?; + + if email_text.is_empty() { + Err("Failed to extract email content".into()) + } else { + Ok(email_text) + } +} -// Ok(HttpResponse::Ok().json(serde_json::json!({ -// "message": "Email archived successfully", -// "email_id": email_id, -// "archive_folder": "Archive" -// }))) -// } #[actix_web::post("/emails/send")] pub async fn send_email( diff --git a/src/services/keywords/create_draft.rs b/src/services/keywords/create_draft.rs index 106e482..1146b6d 100644 --- a/src/services/keywords/create_draft.rs +++ b/src/services/keywords/create_draft.rs @@ -1,32 +1,62 @@ +use crate::services::email::SaveDraftRequest; +use crate::services::email::{fetch_latest_email_from_sender, save_email_draft}; +use crate::services::state::AppState; use rhai::Dynamic; use rhai::Engine; -use serde_json::json; -use crate::services::state::AppState; +pub fn create_draft_keyword(state: &AppState, engine: &mut Engine) { + let state_clone = state.clone(); -pub fn create_draft_keyword(_state: &AppState, engine: &mut Engine) { engine .register_custom_syntax( &["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], true, // Statement - |context, inputs| { - if inputs.len() < 3 { - return Err("Not enough arguments for CREATE DRAFT".into()); - } + move |context, inputs| { + // Extract arguments + let to = context.eval_expression_tree(&inputs[0])?.to_string(); + let subject = context.eval_expression_tree(&inputs[1])?.to_string(); + let reply_text = context.eval_expression_tree(&inputs[2])?.to_string(); - let to = context.eval_expression_tree(&inputs[0])?; - let subject = context.eval_expression_tree(&inputs[1])?; - let body = context.eval_expression_tree(&inputs[2])?; + // Execute async operations using the same pattern as FIND + let fut = execute_create_draft(&state_clone, &to, &subject, &reply_text); + let result = + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("Draft creation error: {}", e))?; - let result = json!({ - "command": "create_draft", - "to": to.to_string(), - "subject": subject.to_string(), - "body": body.to_string() - }); - println!("CREATE DRAFT executed: {}", result.to_string()); - Ok(Dynamic::UNIT) + Ok(Dynamic::from(result)) }, ) .unwrap(); } + +async fn execute_create_draft( + state: &AppState, + to: &str, + subject: &str, + reply_text: &str, +) -> Result { + let get_result = fetch_latest_email_from_sender(&state.config.clone().unwrap().email, to.clone()).await; + let email_body = if let Ok(get_result_str) = get_result { + if !get_result_str.is_empty() { + get_result_str + reply_text + } else { + "".to_string() + } + } else { + reply_text.to_string() + }; + + // Create and save draft + let draft_request = SaveDraftRequest { + to: to.to_string(), + subject: subject.to_string(), + cc: None, + text: email_body, + }; + + let save_result = match save_email_draft(&state.config.clone().unwrap().email, &draft_request).await { + Ok(_) => Ok("Draft saved successfully".to_string()), + Err(e) => Err(e.to_string()), + }; + save_result +} diff --git a/src/services/keywords/create_site.rs b/src/services/keywords/create_site.rs index 826a9d7..efb2c06 100644 --- a/src/services/keywords/create_site.rs +++ b/src/services/keywords/create_site.rs @@ -1,11 +1,11 @@ use rhai::Dynamic; use rhai::Engine; -use serde_json::json; +use std::fs; +use std::path::Path; use crate::services::state::AppState; pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) { - engine .register_custom_syntax( &[ @@ -13,28 +13,35 @@ pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) { "$expr$", ], true, // Statement - |context, inputs| { + move |context, inputs| { if inputs.len() < 5 { return Err("Not enough arguments for CREATE SITE".into()); } - let name = context.eval_expression_tree(&inputs[0])?; + let _name = context.eval_expression_tree(&inputs[0])?; let company = context.eval_expression_tree(&inputs[1])?; - let website = context.eval_expression_tree(&inputs[2])?; - let template = context.eval_expression_tree(&inputs[3])?; + let _website = context.eval_expression_tree(&inputs[2])?; + let _template = context.eval_expression_tree(&inputs[3])?; let prompt = context.eval_expression_tree(&inputs[4])?; - let result = json!({ - "command": "create_site", - "name": name.to_string(), - "company": company.to_string(), - "website": website.to_string(), - "template": template.to_string(), - "prompt": prompt.to_string() - }); - println!("CREATE SITE executed: {}", result.to_string()); + // Call the LLM to generate the HTML content + let llm_result = context.call_fn::("chat", (prompt.to_string(),))?; + + // Create the directory structure + let base_path = "/opt/gbo/tenants/pragmatismo/proxy/data/websites/sites.pragmatismo.com.br"; + let site_name = format!("{}bot", company.to_string()); + let full_path = format!("{}/{}", base_path, site_name); + + // Create directory if it doesn't exist + fs::create_dir_all(&full_path).map_err(|e| e.to_string())?; + + // Write the HTML file + let index_path = Path::new(&full_path).join("index.html"); + fs::write(index_path, llm_result).map_err(|e| e.to_string())?; + + println!("Site created at: {}", full_path); Ok(Dynamic::UNIT) }, ) .unwrap(); -} +} \ No newline at end of file diff --git a/src/services/keywords/get_website.rs b/src/services/keywords/get_website.rs new file mode 100644 index 0000000..dffe82d --- /dev/null +++ b/src/services/keywords/get_website.rs @@ -0,0 +1,133 @@ +use crate::services::{state::AppState, web_automation::BrowserPool}; +use rhai::{Dynamic, Engine}; +use std::error::Error; +use std::sync::Arc; +use std::time::Duration; +use thirtyfour::{By, WebDriver}; +use tokio::time::sleep; + +pub fn get_website_keyword(state: &AppState, engine: &mut Engine) { + let browser_pool = state.browser_pool.clone(); // Assuming AppState has browser_pool field + + engine + .register_custom_syntax( + &["GET", "WEBSITE", "$expr$", "$expr$"], + false, + move |context, inputs| { + let search_term = context.eval_expression_tree(&inputs[0])?.to_string(); + let website_hint = context.eval_expression_tree(&inputs[1])?.to_string(); + + println!( + "GET WEBSITE executed - Search: '{}', Hint: '{}'", + search_term, website_hint + ); + + let browser_pool_clone = browser_pool.clone(); + let fut = execute_headless_browser_search( + browser_pool_clone, + &search_term, + &website_hint, + ); + + let result = + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut)) + .map_err(|e| format!("Headless browser search failed: {}", e))?; + + Ok(Dynamic::from(result)) + }, + ) + .unwrap(); +} + +pub async fn execute_headless_browser_search( + browser_pool: Arc, // Adjust path as needed + search_term: &str, + website_hint: &str, +) -> Result> { + println!( + "Starting headless browser search: '{}' targeting '{}'", + search_term, website_hint + ); + + let search_term = search_term.to_string(); + let website_hint = website_hint.to_string(); + + let result = browser_pool + .with_browser(|driver| { + Box::pin(async move { perform_search(driver, &search_term, &website_hint).await }) + }) + .await?; + + Ok(result) +} + +async fn perform_search( + driver: WebDriver, + search_term: &str, + website_hint: &str, +) -> Result> { + // Configure the search query + let query = if website_hint.trim().is_empty() { + search_term.to_string() + } else { + format!("{} site:{}", search_term, website_hint) + }; + + // Navigate to DuckDuckGo + println!("Navigating to DuckDuckGo..."); + driver.goto("https://duckduckgo.com").await?; + + // Wait for search box and type query + println!("Searching for: {}", query); + let search_input = driver.find(By::Name("q")).await?; + search_input.click().await?; + search_input.send_keys(&query).await?; + + // Submit search by pressing Enter + search_input.send_keys("\n").await?; + + // Wait for results to load + driver.find(By::Css(".result")).await?; + sleep(Duration::from_millis(2000)).await; // Give extra time for JS + + // Extract first result link + let results = extract_search_results(&driver).await?; + + if !results.is_empty() { + println!("Found {} results", results.len()); + Ok(results[0].clone()) + } else { + Ok("No results found".to_string()) + } +} + +async fn extract_search_results( + driver: &WebDriver, +) -> Result, Box> { + let mut results = Vec::new(); + + // Try different selectors for search results + let selectors = [ + "a[data-testid='result-title-a']", // Modern DuckDuckGo + ".result__a", // Classic DuckDuckGo + "a.result-link", // Alternative + ".result a[href]", // Generic result links + ]; + + for selector in &selectors { + if let Ok(elements) = driver.find_all(By::Css(selector)).await { + for element in elements { + if let Ok(Some(href)) = element.attr("href").await { + if href.starts_with("http") && !href.contains("duckduckgo.com") { + results.push(href); + } + } + } + if !results.is_empty() { + break; + } + } + } + + Ok(results) +} diff --git a/src/services/keywords/mod.rs b/src/services/keywords/mod.rs index d2e5b10..56601da 100644 --- a/src/services/keywords/mod.rs +++ b/src/services/keywords/mod.rs @@ -3,5 +3,7 @@ pub mod create_site; pub mod find; pub mod for_next; pub mod get; +pub mod get_website; pub mod print; -pub mod set; \ No newline at end of file +pub mod set; +pub mod wait; \ No newline at end of file diff --git a/src/services/keywords/wait.rs b/src/services/keywords/wait.rs new file mode 100644 index 0000000..45b8771 --- /dev/null +++ b/src/services/keywords/wait.rs @@ -0,0 +1,39 @@ +use rhai::{Dynamic, Engine}; +use crate::services::state::AppState; +use std::thread; +use std::time::Duration; + +pub fn wait_keyword(_state: &AppState, engine: &mut Engine) { + engine.register_custom_syntax( + &["WAIT", "$expr$"], + false, // Expression, not statement + move |context, inputs| { + let seconds = context.eval_expression_tree(&inputs[0])?; + + // Convert to number (handle both int and float) + let duration_secs = if seconds.is::() { + seconds.cast::() as f64 + } else if seconds.is::() { + seconds.cast::() + } else { + return Err(format!("WAIT expects a number, got: {}", seconds).into()); + }; + + if duration_secs < 0.0 { + return Err("WAIT duration cannot be negative".into()); + } + + // Cap maximum wait time to prevent abuse (e.g., 5 minutes max) + let capped_duration = if duration_secs > 300.0 { 300.0 } else { duration_secs }; + + println!("WAIT {} seconds (thread sleep)", capped_duration); + + // Use thread::sleep to block only the current thread, not the entire server + let duration = Duration::from_secs_f64(capped_duration); + thread::sleep(duration); + + println!("WAIT completed after {} seconds", capped_duration); + Ok(Dynamic::from(format!("Waited {} seconds", capped_duration))) + } + ).unwrap(); +} \ No newline at end of file diff --git a/src/services/script.rs b/src/services/script.rs index 215b879..7dc661d 100644 --- a/src/services/script.rs +++ b/src/services/script.rs @@ -4,8 +4,10 @@ use crate::services::keywords::create_site::create_site_keyword; use crate::services::keywords::find::{find_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; use crate::services::keywords::print::print_keyword; use crate::services::keywords::set::set_keyword; +use crate::services::keywords::wait::wait_keyword; use crate::services::state::AppState; pub struct ScriptService { @@ -25,7 +27,9 @@ impl ScriptService { find_keyword(state, &mut engine); for_keyword(state, &mut engine); get_keyword(state, &mut engine); + get_website_keyword(state, &mut engine); set_keyword(state, &mut engine); + wait_keyword(state, &mut engine); print_keyword(state, &mut engine); ScriptService { engine } } diff --git a/src/services/state.rs b/src/services/state.rs index 0b6ae8c..2198b4a 100644 --- a/src/services/state.rs +++ b/src/services/state.rs @@ -1,13 +1,16 @@ +use std::sync::Arc; + use minio::s3::Client; -use crate::services::config::AppConfig; +use crate::services::{config::AppConfig, web_automation::BrowserPool}; -// App state shared across all handlers +#[derive(Clone)] pub struct AppState { pub minio_client: Option, pub config: Option, pub db: Option, pub db_custom: Option, + pub browser_pool: Arc, } diff --git a/src/services/web_automation.rs b/src/services/web_automation.rs new file mode 100644 index 0000000..e168350 --- /dev/null +++ b/src/services/web_automation.rs @@ -0,0 +1,178 @@ +use std::error::Error; +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::process::Command; +use std::sync::Arc; +use thirtyfour::{DesiredCapabilities, WebDriver}; +use tokio::fs; +use tokio::sync::Semaphore; + +pub struct BrowserSetup { + pub brave_path: String, + pub chromedriver_path: String, +} + +pub struct BrowserPool { + webdriver_url: String, + semaphore: Semaphore, + brave_path: String, +} + +impl BrowserPool { + pub fn new(webdriver_url: String, max_concurrent: usize, brave_path: String) -> Self { + Self { + webdriver_url, + semaphore: Semaphore::new(max_concurrent), + brave_path, + } + } + + pub async fn with_browser(&self, f: F) -> Result> + where + F: FnOnce( + WebDriver, + ) + -> Pin>> + Send>> + + Send + + 'static, + T: Send + 'static, + { + let _permit = self.semaphore.acquire().await?; + + let mut caps = DesiredCapabilities::chrome(); + caps.set_binary(&self.brave_path)?; + caps.add_chrome_arg("--headless=new")?; + caps.add_chrome_arg("--disable-gpu")?; + caps.add_chrome_arg("--no-sandbox")?; + + let driver = WebDriver::new(&self.webdriver_url, caps).await?; + + // Execute user function + let result = f(driver).await; + + + result + } +} + +impl BrowserSetup { + pub async fn new() -> Result> { + // Check for Brave installation + let brave_path = Self::find_brave().await?; + + // Check for chromedriver + let chromedriver_path = Self::setup_chromedriver().await?; + + Ok(Self { + brave_path, + chromedriver_path, + }) + } + + 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"), + ]; + + 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> { + let chromedriver_path = String::from(if cfg!(target_os = "windows") { + "chromedriver.exe" + } else { + "chromedriver" + }); + + // Check if chromedriver exists + if fs::metadata(&chromedriver_path).await.is_err() { + println!("Downloading chromedriver..."); + + // Note: This URL structure is outdated. Consider using Chrome for Testing endpoints + let (base_url, platform) = + match (cfg!(target_os = "windows"), cfg!(target_arch = "x86_64")) { + (true, true) => ( + "https://chromedriver.storage.googleapis.com/114.0.5735.90", + "win32", + ), + (false, true) if cfg!(target_os = "macos") => ( + "https://chromedriver.storage.googleapis.com/114.0.5735.90", + "mac64", + ), + (false, true) => ( + "https://chromedriver.storage.googleapis.com/114.0.5735.90", + "linux64", + ), + _ => return Err("Unsupported platform".into()), + }; + + let _download_url = format!("{}/chromedriver_{}.zip", base_url, platform); + + let zip_path = Path::new("chromedriver.zip"); + + + // Clean up zip file + let _ = fs::remove_file(&zip_path).await; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&chromedriver_path).await?.permissions(); + perms.set_mode(0o755); // Make executable + fs::set_permissions(&chromedriver_path, perms).await?; + } + } + + Ok(chromedriver_path) + } +} + +// Modified BrowserPool initialization +pub async fn initialize_browser_pool() -> Result, Box> { + let setup = BrowserSetup::new().await?; + + // Start chromedriver process if not running + if !is_process_running("chromedriver").await { + Command::new(&setup.chromedriver_path) + .arg("--port=9515") + .spawn()?; + + // Give chromedriver time to start + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } + + Ok(Arc::new(BrowserPool::new( + "http://localhost:9515".to_string(), + 5, // Max concurrent browsers + setup.brave_path, + ))) +} + +async fn is_process_running(name: &str) -> bool { + if cfg!(target_os = "windows") { + Command::new("tasklist") + .output() + + .map(|o| String::from_utf8_lossy(&o.stdout).contains(name)) + .unwrap_or(false) + } else { + Command::new("pgrep") + .arg(name) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } +}