- CREATE DRAFT, CREATE SITE, GET WEBSITE, WAIT keywords added.
All checks were successful
GBCI / build (push) Successful in 18m14s
All checks were successful
GBCI / build (push) Successful in 18m14s
This commit is contained in:
parent
90016ea373
commit
ed4aad72f4
15 changed files with 890 additions and 140 deletions
250
Cargo.lock
generated
250
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
10
src/main.rs
10
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());
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,6 @@ pub struct AppConfig {
|
|||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub database_custom: DatabaseConfig,
|
||||
|
||||
pub email: EmailConfig,
|
||||
pub ai: AIConfig,
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
133
src/services/keywords/get_website.rs
Normal file
133
src/services/keywords/get_website.rs
Normal 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)
|
||||
}
|
|
@ -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;
|
39
src/services/keywords/wait.rs
Normal file
39
src/services/keywords/wait.rs
Normal 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();
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
178
src/services/web_automation.rs
Normal file
178
src/services/web_automation.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue