- CREATE DRAFT, CREATE SITE, GET WEBSITE, WAIT keywords added.
All checks were successful
GBCI / build (push) Successful in 18m14s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-07-21 12:16:17 -03:00
parent 90016ea373
commit ed4aad72f4
15 changed files with 890 additions and 140 deletions

250
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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());

View file

@ -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

View file

@ -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;
pub mod script;
pub mod state;
pub mod utils;
pub mod web_automation;

View file

@ -6,7 +6,6 @@ pub struct AppConfig {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub database_custom: DatabaseConfig,
pub email: EmailConfig,
pub ai: AIConfig,
}

View file

@ -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<String>,
// state: web::Data<AppState>,
// ) -> Result<HttpResponse, actix_web::Error> {
// 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<String>,
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<String>,
}
// // 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<String>,
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<AppState>,
draft_data: web::Json<SaveDraftRequest>,
) -> Result<web::Json<SaveDraftResponse>, 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<String, Box<dyn std::error::Error>> {
// 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::<Vec<_>>().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<String>,
// state: web::Data<AppState>,
// ) -> Result<HttpResponse, actix_web::Error> {
// 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<AppState>,
request: web::Json<GetLatestEmailRequest>,
) -> Result<web::Json<LatestEmailResponse>, 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<String, Box<dyn std::error::Error>> {
// 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(

View file

@ -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<String, String> {
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
}

View file

@ -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::<String>("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();
}
}

View file

@ -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<BrowserPool>, // Adjust path as needed
search_term: &str,
website_hint: &str,
) -> Result<String, Box<dyn Error + Send + Sync>> {
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<String, Box<dyn Error + Send + Sync>> {
// 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<Vec<String>, Box<dyn Error + Send + Sync>> {
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)
}

View file

@ -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;
pub mod set;
pub mod wait;

View file

@ -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::<i64>() {
seconds.cast::<i64>() as f64
} else if seconds.is::<f64>() {
seconds.cast::<f64>()
} 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();
}

View file

@ -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 }
}

View file

@ -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<Client>,
pub config: Option<AppConfig>,
pub db: Option<sqlx::PgPool>,
pub db_custom: Option<sqlx::PgPool>,
pub browser_pool: Arc<BrowserPool>,
}

View file

@ -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<F, T>(&self, f: F) -> Result<T, Box<dyn Error + Send + Sync>>
where
F: FnOnce(
WebDriver,
)
-> Pin<Box<dyn Future<Output = Result<T, Box<dyn Error + Send + Sync>>> + 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<Self, Box<dyn std::error::Error>> {
// 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<String, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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<Arc<BrowserPool>, Box<dyn std::error::Error>> {
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)
}
}