From d4697a6a93fefdf8af37823f7975b1449cd53b10 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 14 Jul 2025 16:34:09 -0300 Subject: [PATCH] Add container setup scripts for various services - Implemented ALM container setup with Forgejo installation and systemd service configuration. - Created Bot container setup with necessary dependencies and Node.js application installation. - Developed Desktop container setup with XRDP and Brave browser installation. - Established Directory container setup with Zitadel installation and service configuration. - Added Doc Editor container setup for Collabora Online integration. - Implemented Drive container setup with MinIO installation and service configuration. - Created Email container setup with Stalwart Mail installation and service configuration. - Developed Meeting container setup with LiveKit and TURN server configuration. - Added Proxy container setup with Caddy installation and service configuration. - Implemented System container setup for general bots with service configuration. - Created Table Editor container setup with NocoDB installation and service configuration. - Developed Tables container setup with PostgreSQL installation and configuration. - Added Webmail container setup with Roundcube installation and service configuration. - Included prompt guidelines for container setup scripts. --- Cargo.lock | 85 +++++++++ Cargo.toml | 1 + src/main.rs | 22 ++- src/scripts/containers/bot.sh | 4 + src/scripts/containers/dns.sh | 3 + src/scripts/containers/email.sh | 6 +- src/scripts/utils/set-limits.sh | 20 +- src/services.rs | 3 +- src/services/llm.rs | 6 +- src/services/script.rs | 311 ++++++++++++++++++++++++++++++++ 10 files changed, 446 insertions(+), 15 deletions(-) create mode 100644 src/services/script.rs diff --git a/Cargo.lock b/Cargo.lock index b40ba44..76eb6ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "const-random", "getrandom 0.3.3", "once_cell", "version_check", @@ -883,6 +884,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -974,6 +995,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1625,6 +1652,7 @@ dependencies = [ "minio", "native-tls", "reqwest 0.11.27", + "rhai", "serde", "serde_json", "sqlx", @@ -2845,6 +2873,9 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -3477,6 +3508,34 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "rhai" +version = "1.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249" +dependencies = [ + "ahash", + "bitflags 2.9.1", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "ring" version = "0.17.14" @@ -3900,6 +3959,17 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.4.10" @@ -4370,6 +4440,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + [[package]] name = "thiserror" version = "1.0.69" @@ -4465,6 +4541,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index c91e08f..61d19ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ actix-ws="0.3.0" bytes = "1.1" futures-util = "0.3" reqwest = { version = "0.11", features = ["json", "stream"] } +rhai = "1.0" chrono = { version = "0.4", features = ["serde"] } dotenv = "0.15" diff --git a/src/main.rs b/src/main.rs index bf0e94f..989150c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,8 @@ use actix_web::http::header; use actix_web::{web, App, HttpServer}; use dotenv::dotenv; - +use services::script +::*; use services::config::*; use services::email::*; use services::file::*; @@ -17,6 +18,25 @@ async fn main() -> std::io::Result<()> { dotenv().ok(); let config = AppConfig::from_env(); + let script_service = ScriptService::new(); + + let script = r#" + let json = FIND "users", "name=John" + let x=2 + let text = GET "example.com" + "#; + + match script_service.compile(script) { + Ok(ast) => { + match script_service.run(&ast) { + Ok(result) => println!("Script executed successfully: {:?}", result), + Err(e) => eprintln!("Error executing script: {}", e), + } + }, + Err(e) => eprintln!("Error compiling script: {}", e), + } + + let db_url = config.database_url(); //let db = PgPool::connect(&db_url).await.unwrap(); diff --git a/src/scripts/containers/bot.sh b/src/scripts/containers/bot.sh index 5925f94..ea455c8 100644 --- a/src/scripts/containers/bot.sh +++ b/src/scripts/containers/bot.sh @@ -12,6 +12,10 @@ lxc launch images:debian/12 "$PARAM_TENANT"-bot -c security.privileged=true sleep 15 lxc exec "$PARAM_TENANT"-bot -- bash -c " + +echo "nameserver $PARAM_DNS_INTERNAL_IP" > /etc/resolv.conf + + apt-get update && apt-get install -y \ build-essential cmake git pkg-config libjpeg-dev libtiff-dev \ libpng-dev libavcodec-dev libavformat-dev libswscale-dev \ diff --git a/src/scripts/containers/dns.sh b/src/scripts/containers/dns.sh index ceb8666..9fd95c0 100644 --- a/src/scripts/containers/dns.sh +++ b/src/scripts/containers/dns.sh @@ -6,6 +6,9 @@ HOST_LOGS="$HOST_BASE/logs" mkdir -p "$HOST_BASE" "$HOST_CONF" "$HOST_DATA" "$HOST_LOGS" chmod -R 750 "$HOST_BASE" +lxc network set lxdbr0 user.dns.nameservers $PARAM_DNS_INTERNAL_IP,8.8.8.8,1.1.1.1 +lxc network set lxdbr0 dns.mode managed + # Clear existing rules sudo iptables -F diff --git a/src/scripts/containers/email.sh b/src/scripts/containers/email.sh index f77792f..81f583c 100644 --- a/src/scripts/containers/email.sh +++ b/src/scripts/containers/email.sh @@ -37,9 +37,13 @@ sleep 15 echo "[CONTAINER] Installing Stalwart Mail..." lxc exec "$PARAM_TENANT"-email -- bash -c " + +echo "nameserver $PARAM_DNS_INTERNAL_IP" > /etc/resolv.conf + +apt install resolvconf -y apt-get update && apt-get install -y wget libcap2-bin wget -O /tmp/stalwart.tar.gz https://github.com/stalwartlabs/stalwart/releases/download/v0.12.4/stalwart-x86_64-unknown-linux-gnu.tar.gz - + tar -xzf /tmp/stalwart.tar.gz -C /tmp mkdir -p /opt/gbo/bin mv /tmp/stalwart /opt/gbo/bin/stalwart diff --git a/src/scripts/utils/set-limits.sh b/src/scripts/utils/set-limits.sh index e0002cc..33d26d2 100644 --- a/src/scripts/utils/set-limits.sh +++ b/src/scripts/utils/set-limits.sh @@ -3,16 +3,18 @@ # Define container limits in an associative array declare -A container_limits=( # Pattern Memory CPU Allowance - ["*tables*"]="2048MB:25ms/100ms" - ["*proxy*"]="512MB:25ms/100ms" - ["*directory*"]="512MB:25ms/100ms" - ["*drive*"]="1024MB:25ms/100ms" - ["*email*"]="1024MB:20ms/100ms" - ["*webmail*"]="1024MB:20ms/100ms" + ["*tables*"]="2048MB:50ms/100ms" + ["*dns*"]="512MB:50ms/100ms" + ["*doc-editor*"]="512MB:50ms/100ms" + ["*proxy*"]="512MB:50ms/100ms" + ["*directory*"]="512MB:50ms/100ms" + ["*drive*"]="1024MB:50ms/100ms" + ["*email*"]="2048MB:90ms/100ms" + ["*webmail*"]="2048MB:90ms/100ms" ["*bot*"]="2048MB:50ms/100ms" - ["*meeting*"]="1024MB:20ms/100ms" - ["*alm*"]="512MB:20ms/100ms" - ["*alm-ci*"]="4096MB:80ms/100ms" + ["*meeting*"]="1024MB:50ms/100ms" + ["*alm*"]="512MB:50ms/100ms" + ["*alm-ci*"]="4096MB:50ms/100ms" ) # Default values (for containers that don't match any pattern) diff --git a/src/services.rs b/src/services.rs index 0ef8ec3..00342cf 100644 --- a/src/services.rs +++ b/src/services.rs @@ -2,4 +2,5 @@ pub mod config; pub mod file; pub mod state; pub mod email; -pub mod llm; \ No newline at end of file +pub mod llm; +pub mod script; \ No newline at end of file diff --git a/src/services/llm.rs b/src/services/llm.rs index e756e0e..d4cafcd 100644 --- a/src/services/llm.rs +++ b/src/services/llm.rs @@ -59,7 +59,7 @@ pub async fn chat( let open_ai = OpenAI::new(azure_config); // Parse the context JSON - let context: serde_json::Value = match serde_json::from_str(&request.context) { + let context: serde_json::Value = match serde_json::from_str(&request) { Ok(ctx) => ctx, Err(_) => serde_json::json!({}) }; @@ -71,11 +71,11 @@ pub async fn chat( format!( "Respond to this email: {}. Keep it professional and concise. \ If the email requires a response, provide one in the 'replyEmail' action format.", - request.input + request ), true, ), - _ => (request.input, false), + _ => (request, false), }; let response_text = match open_ai.invoke(&prompt).await { diff --git a/src/services/script.rs b/src/services/script.rs new file mode 100644 index 0000000..8b84d52 --- /dev/null +++ b/src/services/script.rs @@ -0,0 +1,311 @@ +use rhai::module_resolvers::StaticModuleResolver; +use rhai::{ + Dynamic, Engine, EvalAltResult, ImmutableString, LexError, ParseError, ParseErrorType, Position, +}; +use serde_json::json; +use std::collections::HashMap; + +pub struct ScriptService { + engine: Engine, + module_resolver: StaticModuleResolver, +} + +impl ScriptService { + pub fn new() -> Self { + let mut engine = Engine::new(); + let module_resolver = StaticModuleResolver::new(); + + // Configure engine for BASIC-like syntax + engine.set_allow_anonymous_fn(true); + engine.set_allow_looping(true); + + // Register custom syntax for FOR EACH loop + engine.register_custom_syntax( + &["FOR", "EACH", "$ident$", "in", "$expr$", "$block$"], + false, // Not a statement + |context, inputs| { + // Simple implementation - just return unit for now + Ok(Dynamic::UNIT) + }, + ).unwrap(); + + // FIND command: FIND "table", "filter" + engine.register_custom_syntax( + &["FIND", "$expr$", ",", "$expr$"], + false, // Expression, not statement + |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let filter = context.eval_expression_tree(&inputs[1])?; + + let table_str = table_name.to_string(); + let filter_str = filter.to_string(); + + let result = json!({ + "command": "find", + "table": table_str, + "filter": filter_str, + "results": [] + }); + Ok(Dynamic::from(result.to_string())) + }, + ).unwrap(); + + // SET command: SET "table", "key", "value" + engine.register_custom_syntax( + &["SET", "$expr$", ",", "$expr$", ",", "$expr$"], + true, // Statement + |context, inputs| { + let table_name = context.eval_expression_tree(&inputs[0])?; + let key_value = context.eval_expression_tree(&inputs[1])?; + let value = context.eval_expression_tree(&inputs[2])?; + + let table_str = table_name.to_string(); + let key_str = key_value.to_string(); + let value_str = value.to_string(); + + let result = json!({ + "command": "set", + "status": "success", + "table": table_str, + "key": key_str, + "value": value_str + }); + println!("SET executed: {}", result.to_string()); + Ok(Dynamic::UNIT) + }, + ).unwrap(); + + // GET command: GET "url" + engine.register_custom_syntax( + &["GET", "$expr$"], + false, // Expression, not statement + |context, inputs| { + let url = context.eval_expression_tree(&inputs[0])?; + let url_str = url.to_string(); + + Ok(format!("Content from {}", url_str).into()) + }, + ).unwrap(); + + // CREATE SITE command: CREATE SITE "name", "company", "website", "template", "prompt" + engine.register_custom_syntax( + &["CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$"], + true, // Statement + |context, inputs| { + if inputs.len() < 5 { + return Err("Not enough arguments for CREATE SITE".into()); + } + + 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 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()); + Ok(Dynamic::UNIT) + }, + ).unwrap(); + + // CREATE DRAFT command: CREATE DRAFT "to", "subject", "body" + 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()); + } + + let to = context.eval_expression_tree(&inputs[0])?; + let subject = context.eval_expression_tree(&inputs[1])?; + let body = context.eval_expression_tree(&inputs[2])?; + + 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) + }, + ).unwrap(); + + // PRINT command + engine.register_custom_syntax( + &["PRINT", "$expr$"], + true, // Statement + |context, inputs| { + let value = context.eval_expression_tree(&inputs[0])?; + println!("{}", value); + Ok(Dynamic::UNIT) + }, + ).unwrap(); + + // Register web service functions + engine.register_fn("web_get", |url: &str| { + format!("Response from {}", url) + }); + + ScriptService { + engine, + module_resolver, + } + } + + /// Preprocesses BASIC-style script to handle semicolon-free syntax + fn preprocess_basic_script(&self, script: &str) -> String { + let mut result = String::new(); + let mut in_block = false; + + for line in script.lines() { + let trimmed = line.trim(); + + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") { + result.push_str(line); + result.push('\n'); + continue; + } + + // Track block state + if trimmed.contains('{') { + in_block = true; + } + if trimmed.contains('}') { + in_block = false; + } + + // Check if line starts with our custom commands (these don't need semicolons) + let custom_commands = ["SET", "CREATE", "PRINT", "FOR", "FIND", "GET"]; + let is_custom_command = custom_commands.iter().any(|&cmd| trimmed.starts_with(cmd)); + + if is_custom_command || in_block { + // Custom commands and block content don't need semicolons + result.push_str(line); + } else { + // Regular statements need semicolons + result.push_str(line); + if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') { + result.push(';'); + } + } + result.push('\n'); + } + + result + } +pub fn compile(&self, script: &str) -> Result> { + let processed_script = self.preprocess_basic_script(script); + match self.engine.compile(&processed_script) { + Ok(ast) => Ok(ast), + Err(parse_error) => Err(Box::new(EvalAltResult::from(parse_error))), + } +} + + + pub fn run(&self, ast: &rhai::AST) -> Result> { + self.engine.eval_ast(ast) + } + + pub fn call_web_service( + &self, + endpoint: &str, + data: HashMap, + ) -> Result> { + Ok(format!("Called {} with {:?}", endpoint, data)) + } + + /// Execute a BASIC-style script without semicolons + pub fn execute_basic_script(&self, script: &str) -> Result> { + let processed = self.preprocess_basic_script(script); + let ast = self.engine.compile(&processed)?; + self.run(&ast) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_script_without_semicolons() { + let service = ScriptService::new(); + + // Test BASIC-style script without semicolons + let script = r#" +json = FIND "users", "name=John" +SET "users", "name=John", "age=30" +text = GET "example.com" +CREATE SITE "mysite", "My Company", "mycompany.com", "basic", "Create a professional site" +CREATE DRAFT "client@example.com", "Project Update", "Here's the latest update..." +PRINT "Script completed successfully" + "#; + + let result = service.execute_basic_script(script); + assert!(result.is_ok()); + } + + #[test] + fn test_preprocessing() { + let service = ScriptService::new(); + + let script = r#" +json = FIND "users", "name=John" +SET "users", "name=John", "age=30" +let x = 42 +PRINT x +if x > 10 { + PRINT "Large number" +} + "#; + + let processed = service.preprocess_basic_script(script); + + // Should add semicolons to regular statements but not custom commands + assert!(processed.contains("let x = 42;")); + assert!(processed.contains("json = FIND")); + assert!(!processed.contains("SET \"users\"")); + assert!(!processed.contains("PRINT \"Large number\";")); // Inside block shouldn't get semicolon + } + + #[test] + fn test_individual_commands() { + let service = ScriptService::new(); + + let commands = vec![ + r#"SET "users", "name=John", "age=30""#, + r#"CREATE SITE "mysite", "My Company", "mycompany.com", "basic", "Create a professional site""#, + r#"CREATE DRAFT "client@example.com", "Project Update", "Here's the latest update...""#, + r#"PRINT "Hello, World!""#, + ]; + + for cmd in commands { + let result = service.execute_basic_script(cmd); + assert!(result.is_ok(), "Command '{}' failed", cmd); + } + } + + #[test] + fn test_block_statements() { + let service = ScriptService::new(); + + let script = r#" +if true { + PRINT "Inside block" + PRINT "Another statement" +} + "#; + + let result = service.execute_basic_script(script); + assert!(result.is_ok()); + } +} \ No newline at end of file