- 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 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]] [[package]]
name = "dtoa" name = "dtoa"
version = "1.0.10" version = "1.0.10"
@ -1418,6 +1431,28 @@ dependencies = [
"regex", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -1451,7 +1486,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -1641,6 +1676,7 @@ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"dotenv", "dotenv",
"downloader",
"env_logger 0.10.2", "env_logger 0.10.2",
"futures", "futures",
"futures-util", "futures-util",
@ -1651,17 +1687,21 @@ dependencies = [
"mailparse", "mailparse",
"minio", "minio",
"native-tls", "native-tls",
"regex",
"reqwest 0.11.27", "reqwest 0.11.27",
"rhai", "rhai",
"scraper 0.18.1",
"serde", "serde",
"serde_json", "serde_json",
"smartstring", "smartstring",
"sqlx", "sqlx",
"tempfile", "tempfile",
"thirtyfour",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"urlencoding",
] ]
[[package]] [[package]]
@ -2018,6 +2058,21 @@ dependencies = [
"want", "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]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.7" version = "0.27.7"
@ -2028,10 +2083,10 @@ dependencies = [
"hyper 1.6.0", "hyper 1.6.0",
"hyper-util", "hyper-util",
"rustls 0.23.28", "rustls 0.23.28",
"rustls-native-certs", "rustls-native-certs 0.8.1",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls 0.26.2",
"tower-service", "tower-service",
] ]
@ -2409,7 +2464,7 @@ dependencies = [
"regex", "regex",
"reqwest 0.12.20", "reqwest 0.12.20",
"reqwest-eventsource", "reqwest-eventsource",
"scraper", "scraper 0.20.0",
"secrecy", "secrecy",
"serde", "serde",
"serde_json", "serde_json",
@ -2435,7 +2490,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [ dependencies = [
"spin", "spin 0.9.8",
] ]
[[package]] [[package]]
@ -3240,7 +3295,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"lru-slab", "lru-slab",
"rand 0.9.1", "rand 0.9.1",
"ring", "ring 0.17.14",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls 0.23.28", "rustls 0.23.28",
"rustls-pki-types", "rustls-pki-types",
@ -3461,7 +3516,7 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls 0.27.7",
"hyper-tls 0.6.0", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
@ -3473,7 +3528,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls 0.23.28", "rustls 0.23.28",
"rustls-native-certs", "rustls-native-certs 0.8.1",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
@ -3481,7 +3536,7 @@ dependencies = [
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls", "tokio-rustls 0.26.2",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@ -3537,6 +3592,21 @@ dependencies = [
"syn 2.0.103", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@ -3547,7 +3617,7 @@ dependencies = [
"cfg-if", "cfg-if",
"getrandom 0.2.16", "getrandom 0.2.16",
"libc", "libc",
"untrusted", "untrusted 0.9.0",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3611,13 +3681,25 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "rustls" name = "rustls"
version = "0.21.12" version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [ dependencies = [
"ring", "ring 0.17.14",
"rustls-webpki 0.101.7", "rustls-webpki 0.101.7",
"sct", "sct",
] ]
@ -3629,13 +3711,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring", "ring 0.17.14",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki 0.103.3", "rustls-webpki 0.103.3",
"subtle", "subtle",
"zeroize", "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]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.8.1" version = "0.8.1"
@ -3673,8 +3767,8 @@ version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [ dependencies = [
"ring", "ring 0.17.14",
"untrusted", "untrusted 0.9.0",
] ]
[[package]] [[package]]
@ -3683,9 +3777,9 @@ version = "0.103.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
dependencies = [ dependencies = [
"ring", "ring 0.17.14",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted 0.9.0",
] ]
[[package]] [[package]]
@ -3715,6 +3809,22 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "scraper" name = "scraper"
version = "0.20.0" version = "0.20.0"
@ -3737,8 +3847,8 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [ dependencies = [
"ring", "ring 0.17.14",
"untrusted", "untrusted 0.9.0",
] ]
[[package]] [[package]]
@ -3838,6 +3948,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@ -3853,6 +3964,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -3991,6 +4113,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -4254,6 +4382,15 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "stringmatch"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c0faab770316c3838f895fc2dfc3a8707ef4da48676f1014e1061ebd583b40"
dependencies = [
"regex",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"
@ -4447,6 +4584,31 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" 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]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@ -4615,6 +4777,17 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.2" version = "0.26.2"
@ -4821,6 +4994,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -4844,6 +5023,12 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlparse"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -5026,6 +5211,35 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.25.4" version = "0.25.4"

View file

@ -12,6 +12,8 @@ actix-cors = "0.6"
actix-multipart = "0.6" actix-multipart = "0.6"
actix-web = "4" actix-web = "4"
actix-ws="0.3.0" actix-ws="0.3.0"
thirtyfour = { version = "0.30" }
downloader = "0.2.8"
anyhow = "1.0" anyhow = "1.0"
async-stream = "0.3" async-stream = "0.3"
bytes = "1.1" bytes = "1.1"
@ -38,3 +40,6 @@ tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] } 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_cors::Cors;
use actix_web::http::header; use actix_web::http::header;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
@ -10,6 +12,8 @@ use services::llm::*;
use services::script::*; use services::script::*;
use services::state::*; use services::state::*;
use sqlx::PgPool; use sqlx::PgPool;
use crate::services::web_automation::BrowserPool;
//use services:: find::*; //use services:: find::*;
mod services; mod services;
@ -29,11 +33,17 @@ async fn main() -> std::io::Result<()> {
.await .await
.expect("Failed to initialize Minio"); .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 { let app_state = web::Data::new(AppState {
db: db.into(), db: db.into(),
db_custom: db_custom.into(), db_custom: db_custom.into(),
config: Some(config.clone()), config: Some(config.clone()),
minio_client: minio_client.into(), minio_client: minio_client.into(),
browser_pool: browser_pool.clone(),
}); });
let script_service = ScriptService::new(&app_state.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 FOR EACH item IN items
let text = GET "https://pragmatismo.com.br"
PRINT item.company 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 NEXT item

View file

@ -1,8 +1,9 @@
pub mod config; pub mod config;
pub mod utils;
pub mod state;
pub mod email; pub mod email;
pub mod keywords;
pub mod file; pub mod file;
pub mod keywords;
pub mod llm; 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 server: ServerConfig,
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub database_custom: DatabaseConfig, pub database_custom: DatabaseConfig,
pub email: EmailConfig, pub email: EmailConfig,
pub ai: AIConfig, pub ai: AIConfig,
} }

View file

@ -160,102 +160,211 @@ fn parse_from_field(from: &str) -> (String, String) {
("Unknown".to_string(), from.to_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 #[derive(serde::Serialize)]
// // .select("INBOX") pub struct SaveDraftResponse {
// // .await pub success: bool,
// // .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?; pub message: String,
pub draft_id: Option<String>,
}
// // let messages = session #[derive(serde::Deserialize)]
// // .fetch(&email_id, "RFC822.HEADER BODY[TEXT]") pub struct GetLatestEmailRequest {
// // .await pub from_email: String,
// // .map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?; }
// // let msg = messages #[derive(serde::Serialize)]
// // .iter() pub struct LatestEmailResponse {
// // .next() pub success: bool,
// // .ok_or_else(|| actix_web::error::ErrorNotFound("Email not found"))?; pub email_text: Option<String>,
pub message: String,
}
// // let header = msg #[actix_web::post("/emails/save_draft")]
// // .header() pub async fn save_draft(
// // .ok_or_else(|| ErrorInternalServerError("No header found"))?; 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 match save_email_draft(&config.email, &draft_data).await {
// // .text() Ok(draft_id) => Ok(web::Json(SaveDraftResponse {
// // .ok_or_else(|| ErrorInternalServerError("No body found"))?; 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() { pub async fn save_email_draft(
// // if line.starts_with("Subject: ") { email_config: &EmailConfig,
// // subject = line.strip_prefix("Subject: ").unwrap_or("").to_string(); draft_data: &SaveDraftRequest,
// // } else if line.starts_with("From: ") { ) -> Result<String, Box<dyn std::error::Error>> {
// // from_info = line.strip_prefix("From: ").unwrap_or("").to_string(); // 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!({ // Select or create Drafts folder
// // "suggested_response": "Thank you for your email. I will review this and get back to you shortly.", if session.select("Drafts").is_err() {
// // "prompt": format!( // Try to create Drafts folder if it doesn't exist
// // "Email from: {}\nSubject: {}\n\nBody:\n{}\n\n---\n\nPlease draft a professional response to this email.", session.create("Drafts")?;
// // from_info, subject, body_text.lines().take(20).collect::<Vec<_>>().join("\n") session.select("Drafts")?;
// // ) }
// // });
// // session.logout().await.ok(); // Create email message
// //Ok(HttpResponse::Ok().json(response)) 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}")] let email_message = format!(
// pub async fn archive_email( "From: {}\r\nTo: {}\r\n{}Subject: {}\r\nDate: {}\r\n\r\n{}",
// path: web::Path<String>, email_config.username,
// state: web::Data<AppState>, draft_data.to,
// ) -> Result<HttpResponse, actix_web::Error> { cc_header,
// let email_id = path.into_inner(); draft_data.subject,
// let config = state chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"),
// .config draft_data.text
// .as_ref() );
// .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
// let mut session = create_imap_session(&config.email).await?; // Append to Drafts folder
session.append("Drafts", &email_message)?;
session.logout()?;
// session Ok(chrono::Utc::now().timestamp().to_string())
// .select("INBOX") }
// .await
// .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
// // Create Archive folder if it doesn't exist #[actix_web::post("/emails/get_latest_from")]
// session.create("Archive").await.ok(); // Ignore error if folder exists 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 match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
// session.mv(&email_id, "Archive").await.map_err(|e| { Ok(email_text) => Ok(web::Json(LatestEmailResponse {
// ErrorInternalServerError(format!("Failed to move email to archive: {:?}", e)) 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")] #[actix_web::post("/emails/send")]
pub async fn send_email( 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::Dynamic;
use rhai::Engine; 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 engine
.register_custom_syntax( .register_custom_syntax(
&["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"], &["CREATE", "DRAFT", "$expr$", ",", "$expr$", ",", "$expr$"],
true, // Statement true, // Statement
|context, inputs| { move |context, inputs| {
if inputs.len() < 3 { // Extract arguments
return Err("Not enough arguments for CREATE DRAFT".into()); 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])?; // Execute async operations using the same pattern as FIND
let subject = context.eval_expression_tree(&inputs[1])?; let fut = execute_create_draft(&state_clone, &to, &subject, &reply_text);
let body = context.eval_expression_tree(&inputs[2])?; 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!({ Ok(Dynamic::from(result))
"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)
}, },
) )
.unwrap(); .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::Dynamic;
use rhai::Engine; use rhai::Engine;
use serde_json::json; use std::fs;
use std::path::Path;
use crate::services::state::AppState; use crate::services::state::AppState;
pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) { pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) {
engine engine
.register_custom_syntax( .register_custom_syntax(
&[ &[
@ -13,28 +13,35 @@ pub fn create_site_keyword(_state: &AppState, engine: &mut Engine) {
"$expr$", "$expr$",
], ],
true, // Statement true, // Statement
|context, inputs| { move |context, inputs| {
if inputs.len() < 5 { if inputs.len() < 5 {
return Err("Not enough arguments for CREATE SITE".into()); 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 company = context.eval_expression_tree(&inputs[1])?;
let website = context.eval_expression_tree(&inputs[2])?; let _website = context.eval_expression_tree(&inputs[2])?;
let template = context.eval_expression_tree(&inputs[3])?; let _template = context.eval_expression_tree(&inputs[3])?;
let prompt = context.eval_expression_tree(&inputs[4])?; let prompt = context.eval_expression_tree(&inputs[4])?;
let result = json!({ // Call the LLM to generate the HTML content
"command": "create_site", let llm_result = context.call_fn::<String>("chat", (prompt.to_string(),))?;
"name": name.to_string(),
"company": company.to_string(), // Create the directory structure
"website": website.to_string(), let base_path = "/opt/gbo/tenants/pragmatismo/proxy/data/websites/sites.pragmatismo.com.br";
"template": template.to_string(), let site_name = format!("{}bot", company.to_string());
"prompt": prompt.to_string() let full_path = format!("{}/{}", base_path, site_name);
});
println!("CREATE SITE executed: {}", result.to_string()); // 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) Ok(Dynamic::UNIT)
}, },
) )
.unwrap(); .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 find;
pub mod for_next; pub mod for_next;
pub mod get; pub mod get;
pub mod get_website;
pub mod print; 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::find::{find_keyword};
use crate::services::keywords::for_next::for_keyword; use crate::services::keywords::for_next::for_keyword;
use crate::services::keywords::get::get_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::print::print_keyword;
use crate::services::keywords::set::set_keyword; use crate::services::keywords::set::set_keyword;
use crate::services::keywords::wait::wait_keyword;
use crate::services::state::AppState; use crate::services::state::AppState;
pub struct ScriptService { pub struct ScriptService {
@ -25,7 +27,9 @@ impl ScriptService {
find_keyword(state, &mut engine); find_keyword(state, &mut engine);
for_keyword(state, &mut engine); for_keyword(state, &mut engine);
get_keyword(state, &mut engine); get_keyword(state, &mut engine);
get_website_keyword(state, &mut engine);
set_keyword(state, &mut engine); set_keyword(state, &mut engine);
wait_keyword(state, &mut engine);
print_keyword(state, &mut engine); print_keyword(state, &mut engine);
ScriptService { engine } ScriptService { engine }
} }

View file

@ -1,13 +1,16 @@
use std::sync::Arc;
use minio::s3::Client; 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 struct AppState {
pub minio_client: Option<Client>, pub minio_client: Option<Client>,
pub config: Option<AppConfig>, pub config: Option<AppConfig>,
pub db: Option<sqlx::PgPool>, pub db: Option<sqlx::PgPool>,
pub db_custom: 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)
}
}