Refactor bootstrap and package manager, add ureq
- Split package manager into separate modules - Expose only the installer API - Simplify BootstrapManager to install components and load config - Pin ureq to 3.1.2 and add ureq‑proto crate - Clean up configuration code and remove legacy comments - Update helper scripts and server start command formatting
This commit is contained in:
parent
e1f9111392
commit
aa69c63cee
11 changed files with 175 additions and 2021 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
|
@ -509,7 +509,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq",
|
"ureq 2.12.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1056,6 +1056,7 @@ dependencies = [
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"ureq 3.1.2",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"zip 2.4.2",
|
"zip 2.4.2",
|
||||||
|
|
@ -5658,6 +5659,36 @@ dependencies = [
|
||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"flate2",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls 0.23.32",
|
||||||
|
"rustls-pemfile 2.2.0",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf-8",
|
||||||
|
"webpki-roots 1.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"http 1.3.1",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.7"
|
version = "2.5.7"
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,4 @@ rand = "0.9.2"
|
||||||
pdf-extract = "0.10.0"
|
pdf-extract = "0.10.0"
|
||||||
scraper = "0.20"
|
scraper = "0.20"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
|
ureq = "3.1.2"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$SCRIPT_DIR"
|
PROJECT_ROOT="$SCRIPT_DIR"
|
||||||
OUTPUT_FILE="/tmp/prompt.out"
|
OUTPUT_FILE="/tmp/prompt.out"
|
||||||
|
|
||||||
rm -f "$OUTPUT_FILE"
|
|
||||||
echo "Consolidated LLM Context" > "$OUTPUT_FILE"
|
echo "Consolidated LLM Context" > "$OUTPUT_FILE"
|
||||||
|
|
||||||
prompts=(
|
prompts=(
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$SCRIPT_DIR"
|
PROJECT_ROOT="$SCRIPT_DIR"
|
||||||
OUTPUT_FILE="/tmp/prompt.out"
|
OUTPUT_FILE="/tmp/prompt.out"
|
||||||
rm $OUTPUT_FILE
|
|
||||||
echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE"
|
echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE"
|
||||||
|
|
||||||
prompts=(
|
prompts=(
|
||||||
|
"./prompts/dev/platform/fix-errors.md"
|
||||||
"./prompts/dev/platform/shared.md"
|
"./prompts/dev/platform/shared.md"
|
||||||
"./Cargo.toml"
|
"./Cargo.toml"
|
||||||
"./prompts/dev/platform/fix-errors.md"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for file in "${prompts[@]}"; do
|
for file in "${prompts[@]}"; do
|
||||||
|
|
@ -22,17 +22,18 @@ dirs=(
|
||||||
#"automation"
|
#"automation"
|
||||||
#"basic"
|
#"basic"
|
||||||
#"bot"
|
#"bot"
|
||||||
"bootstrap"
|
#"bootstrap"
|
||||||
#"channels"
|
#"channels"
|
||||||
#"config"
|
"config"
|
||||||
#"context"
|
#"context"
|
||||||
#"email"
|
#"email"
|
||||||
#"file"
|
#"file"
|
||||||
#"llm"
|
#"llm"
|
||||||
#"llm_legacy"
|
#"llm_legacy"
|
||||||
#"org"
|
#"org"
|
||||||
"session"
|
"package_manager"
|
||||||
"shared"
|
#"session"
|
||||||
|
#"shared"
|
||||||
#"tests"
|
#"tests"
|
||||||
#"tools"
|
#"tools"
|
||||||
#"web_automation"
|
#"web_automation"
|
||||||
|
|
|
||||||
5
gbot.sh
5
gbot.sh
|
|
@ -1,2 +1,3 @@
|
||||||
|
clear && \
|
||||||
clear && cargo build && sudo RUST_BACKTRACE=1 ./target/debug/botserver
|
cargo build && \
|
||||||
|
sudo RUST_BACKTRACE=1 ./target/debug/botserver install tables
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,3 @@ If something, need to be added to a external file, inform it separated.
|
||||||
3. **Respect Cargo.toml** - Check dependencies, editions, and features to avoid compiler errors
|
3. **Respect Cargo.toml** - Check dependencies, editions, and features to avoid compiler errors
|
||||||
4. **Type safety** - Ensure all types match and trait bounds are satisfied
|
4. **Type safety** - Ensure all types match and trait bounds are satisfied
|
||||||
5. **Ownership rules** - Fix borrowing, ownership, and lifetime issues
|
5. **Ownership rules** - Fix borrowing, ownership, and lifetime issues
|
||||||
|
|
||||||
|
|
||||||
MORE RULES:
|
|
||||||
- Return only the modified files as a single `.sh` script using `cat`, so the - code can be restored directly.
|
|
||||||
- You MUST return exactly this example format:
|
|
||||||
```sh
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Restore fixed Rust project
|
|
||||||
|
|
||||||
cat > src/<filenamehere>.rs << 'EOF'
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
// test
|
|
||||||
|
|
||||||
cat > src/<anotherfile>.rs << 'EOF'
|
|
||||||
// Fixed library code
|
|
||||||
pub fn add(a: i32, b: i32) -> i32 {
|
|
||||||
a + b
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ MOST IMPORTANT CODE GENERATION RULES:
|
||||||
- No placeholders, never comment/uncomment code, no explanations, no filler text.
|
- No placeholders, never comment/uncomment code, no explanations, no filler text.
|
||||||
- All code must be complete, professional, production-ready, and follow KISS - principles.
|
- All code must be complete, professional, production-ready, and follow KISS - principles.
|
||||||
- NEVER return placeholders of any kind, NEVER comment code, only CONDENSED REAL PRODUCTION GRADE code.
|
- NEVER return placeholders of any kind, NEVER comment code, only CONDENSED REAL PRODUCTION GRADE code.
|
||||||
|
- REMOTE ALL COMMENTS FROM GENERATED CODE. DO NOT COMMENT AT ALL, NO TALK!
|
||||||
- NEVER say that I have already some part of the code, give me it full again, and working.
|
- NEVER say that I have already some part of the code, give me it full again, and working.
|
||||||
- Always increment logging with (all-in-one-line) info!, debug!, trace! to give birth to the console.
|
- Always increment logging with (all-in-one-line) info!, debug!, trace! to give birth to the console.
|
||||||
- If the output is too large, split it into multiple parts, but always - include the full updated code files.
|
- If the output is too large, split it into multiple parts, but always - include the full updated code files.
|
||||||
|
|
|
||||||
|
|
@ -1,500 +1,58 @@
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::package_manager::{InstallMode, PackageManager};
|
use crate::package_manager::{InstallMode, PackageManager};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use diesel::prelude::*;
|
use log::{debug, info, trace, warn};
|
||||||
use log::{info, warn};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::net::TcpListener;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct BootstrapManager {
|
pub struct BootstrapManager {
|
||||||
mode: InstallMode,
|
pub install_mode: InstallMode,
|
||||||
tenant: String,
|
pub tenant: Option<String>,
|
||||||
base_path: PathBuf,
|
|
||||||
config_values: HashMap<String, String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BootstrapManager {
|
impl BootstrapManager {
|
||||||
pub fn new(mode: InstallMode, tenant: Option<String>) -> Self {
|
pub fn new(install_mode: InstallMode, tenant: Option<String>) -> Self {
|
||||||
let tenant = tenant.unwrap_or_else(|| "default".to_string());
|
info!(
|
||||||
let base_path = if mode == InstallMode::Container {
|
"Initializing BootstrapManager with mode {:?} and tenant {:?}",
|
||||||
PathBuf::from("/opt/gbo")
|
install_mode, tenant
|
||||||
} else {
|
);
|
||||||
PathBuf::from("./botserver-stack")
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
mode,
|
install_mode,
|
||||||
tenant,
|
tenant,
|
||||||
base_path,
|
|
||||||
config_values: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bootstrap(&mut self) -> Result<AppConfig> {
|
pub fn bootstrap(&mut self) -> Result<AppConfig> {
|
||||||
info!(
|
info!("Starting bootstrap process");
|
||||||
"Starting bootstrap process in {:?} mode for tenant {}",
|
|
||||||
self.mode, self.tenant
|
|
||||||
);
|
|
||||||
|
|
||||||
std::fs::create_dir_all(&self.base_path).context("Failed to create base directory")?;
|
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
||||||
|
|
||||||
let pm = PackageManager::new(self.mode.clone(), Some(self.tenant.clone()))?;
|
let required_components = vec!["drive", "cache", "tables", "llm"];
|
||||||
|
|
||||||
info!("Installing core infrastructure components");
|
for component in required_components {
|
||||||
self.install_and_configure_tables(&pm)?;
|
if !pm.is_installed(component) {
|
||||||
self.install_and_configure_drive(&pm)?;
|
info!("Installing required component: {}", component);
|
||||||
self.install_and_configure_cache(&pm)?;
|
futures::executor::block_on(pm.install(component))?;
|
||||||
self.install_and_configure_llm(&pm)?;
|
trace!("Successfully installed component: {}", component);
|
||||||
|
} else {
|
||||||
info!("Creating database schema and storing configuration");
|
debug!("Component {} already installed", component);
|
||||||
let config = self.build_config()?;
|
}
|
||||||
self.initialize_database(&config)?;
|
}
|
||||||
self.store_configuration_in_db(&config)?;
|
|
||||||
|
|
||||||
info!("Bootstrap completed successfully");
|
info!("Bootstrap completed successfully");
|
||||||
|
|
||||||
|
let config = match diesel::Connection::establish(
|
||||||
|
"postgres://botserver:botserver@localhost:5432/botserver",
|
||||||
|
) {
|
||||||
|
Ok(mut conn) => {
|
||||||
|
trace!("Connected to database for config loading");
|
||||||
|
AppConfig::from_database(&mut conn)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to connect to database for config: {}", e);
|
||||||
|
trace!("Falling back to environment configuration");
|
||||||
|
AppConfig::from_env()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_and_configure_tables(&mut self, pm: &PackageManager) -> Result<()> {
|
|
||||||
info!("Installing PostgreSQL tables component");
|
|
||||||
pm.install("tables")?;
|
|
||||||
|
|
||||||
let tables_port = self.find_available_port(5432);
|
|
||||||
let tables_password = self.generate_password();
|
|
||||||
|
|
||||||
self.config_values
|
|
||||||
.insert("TABLES_USERNAME".to_string(), self.tenant.clone());
|
|
||||||
self.config_values
|
|
||||||
.insert("TABLES_PASSWORD".to_string(), tables_password.clone());
|
|
||||||
self.config_values
|
|
||||||
.insert("TABLES_SERVER".to_string(), self.get_service_host("tables"));
|
|
||||||
self.config_values
|
|
||||||
.insert("TABLES_PORT".to_string(), tables_port.to_string());
|
|
||||||
self.config_values
|
|
||||||
.insert("TABLES_DATABASE".to_string(), format!("{}_db", self.tenant));
|
|
||||||
|
|
||||||
self.wait_for_service(&self.get_service_host("tables"), tables_port, 30)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"PostgreSQL configured: {}:{}",
|
|
||||||
self.get_service_host("tables"),
|
|
||||||
tables_port
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_and_configure_drive(&mut self, pm: &PackageManager) -> Result<()> {
|
|
||||||
info!("Installing MinIO drive component");
|
|
||||||
pm.install("drive")?;
|
|
||||||
|
|
||||||
let drive_port = self.find_available_port(9000);
|
|
||||||
let _drive_console_port = self.find_available_port(9001);
|
|
||||||
let drive_user = "minioadmin".to_string();
|
|
||||||
let drive_password = self.generate_password();
|
|
||||||
|
|
||||||
self.config_values.insert(
|
|
||||||
"DRIVE_SERVER".to_string(),
|
|
||||||
format!("{}:{}", self.get_service_host("drive"), drive_port),
|
|
||||||
);
|
|
||||||
self.config_values
|
|
||||||
.insert("DRIVE_ACCESSKEY".to_string(), drive_user.clone());
|
|
||||||
self.config_values
|
|
||||||
.insert("DRIVE_SECRET".to_string(), drive_password.clone());
|
|
||||||
self.config_values
|
|
||||||
.insert("DRIVE_USE_SSL".to_string(), "false".to_string());
|
|
||||||
self.config_values
|
|
||||||
.insert("DRIVE_ORG_PREFIX".to_string(), self.tenant.clone());
|
|
||||||
self.config_values.insert(
|
|
||||||
"DRIVE_BUCKET".to_string(),
|
|
||||||
format!("{}default.gbai", self.tenant),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.wait_for_service(&self.get_service_host("drive"), drive_port, 30)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"MinIO configured: {}:{}",
|
|
||||||
self.get_service_host("drive"),
|
|
||||||
drive_port
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_and_configure_cache(&mut self, pm: &PackageManager) -> Result<()> {
|
|
||||||
info!("Installing Redis cache component");
|
|
||||||
pm.install("cache")?;
|
|
||||||
|
|
||||||
let cache_port = self.find_available_port(6379);
|
|
||||||
|
|
||||||
self.config_values.insert(
|
|
||||||
"CACHE_URL".to_string(),
|
|
||||||
format!("redis://{}:{}/", self.get_service_host("cache"), cache_port),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.wait_for_service(&self.get_service_host("cache"), cache_port, 30)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Redis configured: {}:{}",
|
|
||||||
self.get_service_host("cache"),
|
|
||||||
cache_port
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_and_configure_llm(&mut self, pm: &PackageManager) -> Result<()> {
|
|
||||||
info!("Installing LLM server component");
|
|
||||||
pm.install("llm")?;
|
|
||||||
|
|
||||||
let llm_port = self.find_available_port(8081);
|
|
||||||
|
|
||||||
self.config_values.insert(
|
|
||||||
"LLM_URL".to_string(),
|
|
||||||
format!("http://{}:{}", self.get_service_host("llm"), llm_port),
|
|
||||||
);
|
|
||||||
self.config_values.insert(
|
|
||||||
"AI_ENDPOINT".to_string(),
|
|
||||||
format!("http://{}:{}", self.get_service_host("llm"), llm_port),
|
|
||||||
);
|
|
||||||
self.config_values
|
|
||||||
.insert("AI_KEY".to_string(), "empty".to_string());
|
|
||||||
self.config_values
|
|
||||||
.insert("AI_INSTANCE".to_string(), "llama-local".to_string());
|
|
||||||
self.config_values
|
|
||||||
.insert("AI_VERSION".to_string(), "1.0".to_string());
|
|
||||||
|
|
||||||
self.wait_for_service(&self.get_service_host("llm"), llm_port, 60)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"LLM server configured: {}:{}",
|
|
||||||
self.get_service_host("llm"),
|
|
||||||
llm_port
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_config(&self) -> Result<AppConfig> {
|
|
||||||
info!("Building application configuration from discovered services");
|
|
||||||
|
|
||||||
let get_str = |key: &str, default: &str| -> String {
|
|
||||||
self.config_values
|
|
||||||
.get(key)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| default.to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let get_u32 = |key: &str, default: u32| -> u32 {
|
|
||||||
self.config_values
|
|
||||||
.get(key)
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(default)
|
|
||||||
};
|
|
||||||
|
|
||||||
let get_u16 = |key: &str, default: u16| -> u16 {
|
|
||||||
self.config_values
|
|
||||||
.get(key)
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(default)
|
|
||||||
};
|
|
||||||
|
|
||||||
let get_bool = |key: &str, default: bool| -> bool {
|
|
||||||
self.config_values
|
|
||||||
.get(key)
|
|
||||||
.map(|v| v.to_lowercase() == "true")
|
|
||||||
.unwrap_or(default)
|
|
||||||
};
|
|
||||||
|
|
||||||
let stack_path = self.base_path.clone();
|
|
||||||
|
|
||||||
let database = crate::config::DatabaseConfig {
|
|
||||||
username: get_str("TABLES_USERNAME", "botserver"),
|
|
||||||
password: get_str("TABLES_PASSWORD", "botserver"),
|
|
||||||
server: get_str("TABLES_SERVER", "localhost"),
|
|
||||||
port: get_u32("TABLES_PORT", 5432),
|
|
||||||
database: get_str("TABLES_DATABASE", "botserver_db"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let database_custom = database.clone();
|
|
||||||
|
|
||||||
let minio = crate::config::DriveConfig {
|
|
||||||
server: get_str("DRIVE_SERVER", "localhost:9000"),
|
|
||||||
access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"),
|
|
||||||
secret_key: get_str("DRIVE_SECRET", "minioadmin"),
|
|
||||||
use_ssl: get_bool("DRIVE_USE_SSL", false),
|
|
||||||
org_prefix: get_str("DRIVE_ORG_PREFIX", "botserver"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let email = crate::config::EmailConfig {
|
|
||||||
from: get_str("EMAIL_FROM", "noreply@example.com"),
|
|
||||||
server: get_str("EMAIL_SERVER", "smtp.example.com"),
|
|
||||||
port: get_u16("EMAIL_PORT", 587),
|
|
||||||
username: get_str("EMAIL_USER", "user"),
|
|
||||||
password: get_str("EMAIL_PASS", "pass"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ai = crate::config::AIConfig {
|
|
||||||
instance: get_str("AI_INSTANCE", "llama-local"),
|
|
||||||
key: get_str("AI_KEY", "empty"),
|
|
||||||
version: get_str("AI_VERSION", "1.0"),
|
|
||||||
endpoint: get_str("AI_ENDPOINT", "http://localhost:8081"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let server_host = if self.mode == InstallMode::Container {
|
|
||||||
"0.0.0.0".to_string()
|
|
||||||
} else {
|
|
||||||
"127.0.0.1".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AppConfig {
|
|
||||||
minio,
|
|
||||||
server: crate::config::ServerConfig {
|
|
||||||
host: server_host,
|
|
||||||
port: self.find_available_port(8080),
|
|
||||||
},
|
|
||||||
database,
|
|
||||||
database_custom,
|
|
||||||
email,
|
|
||||||
ai,
|
|
||||||
s3_bucket: get_str("DRIVE_BUCKET", "default.gbai"),
|
|
||||||
site_path: format!("{}/sites", stack_path.display()),
|
|
||||||
stack_path,
|
|
||||||
db_conn: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize_database(&self, config: &AppConfig) -> Result<()> {
|
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
|
|
||||||
info!("Initializing database schema at {}", config.database_url());
|
|
||||||
|
|
||||||
// Attempt to establish a PostgreSQL connection with retries.
|
|
||||||
let mut retries = 5;
|
|
||||||
let mut conn = loop {
|
|
||||||
match PgConnection::establish(&config.database_url()) {
|
|
||||||
Ok(c) => break c,
|
|
||||||
Err(e) if retries > 0 => {
|
|
||||||
warn!("Database connection failed, retrying in 2s: {}", e);
|
|
||||||
thread::sleep(Duration::from_secs(2));
|
|
||||||
retries -= 1;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Failed to connect to database after retries: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the server_configuration table.
|
|
||||||
diesel::sql_query(
|
|
||||||
"CREATE TABLE IF NOT EXISTS server_configuration (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
||||||
config_key TEXT NOT NULL UNIQUE,
|
|
||||||
config_value TEXT NOT NULL,
|
|
||||||
config_type TEXT NOT NULL DEFAULT 'string',
|
|
||||||
is_encrypted BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
)",
|
|
||||||
)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to create server_configuration table")?;
|
|
||||||
|
|
||||||
// Create the bot_configuration table.
|
|
||||||
diesel::sql_query(
|
|
||||||
"CREATE TABLE IF NOT EXISTS bot_configuration (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
||||||
bot_id UUID NOT NULL,
|
|
||||||
config_key TEXT NOT NULL,
|
|
||||||
config_value TEXT NOT NULL,
|
|
||||||
config_type TEXT NOT NULL DEFAULT 'string',
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
UNIQUE(bot_id, config_key)
|
|
||||||
)",
|
|
||||||
)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to create bot_configuration table")?;
|
|
||||||
|
|
||||||
// Create the gbot_config_sync table.
|
|
||||||
diesel::sql_query(
|
|
||||||
"CREATE TABLE IF NOT EXISTS gbot_config_sync (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
||||||
bot_id UUID NOT NULL UNIQUE,
|
|
||||||
config_file_path TEXT NOT NULL,
|
|
||||||
file_hash TEXT NOT NULL,
|
|
||||||
last_sync_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
sync_count INTEGER NOT NULL DEFAULT 0
|
|
||||||
)",
|
|
||||||
)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to create gbot_config_sync table")?;
|
|
||||||
|
|
||||||
info!("Database schema initialized successfully");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn store_configuration_in_db(&self, config: &AppConfig) -> Result<()> {
|
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
|
|
||||||
info!("Storing configuration in database");
|
|
||||||
|
|
||||||
// Establish a PostgreSQL connection explicitly.
|
|
||||||
let mut conn = PgConnection::establish(&config.database_url())
|
|
||||||
.context("Failed to establish database connection for storing configuration")?;
|
|
||||||
|
|
||||||
// Store dynamic configuration values.
|
|
||||||
for (key, value) in &self.config_values {
|
|
||||||
diesel::sql_query(
|
|
||||||
"INSERT INTO server_configuration (config_key, config_value, config_type)
|
|
||||||
VALUES ($1, $2, 'string')
|
|
||||||
ON CONFLICT (config_key)
|
|
||||||
DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()",
|
|
||||||
)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(key)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(value)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.with_context(|| format!("Failed to store config key: {}", key))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store static configuration entries.
|
|
||||||
diesel::sql_query(
|
|
||||||
"INSERT INTO server_configuration (config_key, config_value, config_type)
|
|
||||||
VALUES ('SERVER_HOST', $1, 'string')
|
|
||||||
ON CONFLICT (config_key)
|
|
||||||
DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()",
|
|
||||||
)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(&config.server.host)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to store SERVER_HOST")?;
|
|
||||||
|
|
||||||
diesel::sql_query(
|
|
||||||
"INSERT INTO server_configuration (config_key, config_value, config_type)
|
|
||||||
VALUES ('SERVER_PORT', $1, 'string')
|
|
||||||
ON CONFLICT (config_key)
|
|
||||||
DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()",
|
|
||||||
)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(&config.server.port.to_string())
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to store SERVER_PORT")?;
|
|
||||||
|
|
||||||
diesel::sql_query(
|
|
||||||
"INSERT INTO server_configuration (config_key, config_value, config_type)
|
|
||||||
VALUES ('STACK_PATH', $1, 'string')
|
|
||||||
ON CONFLICT (config_key)
|
|
||||||
DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()",
|
|
||||||
)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(&config.stack_path.display().to_string())
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to store STACK_PATH")?;
|
|
||||||
|
|
||||||
diesel::sql_query(
|
|
||||||
"INSERT INTO server_configuration (config_key, config_value, config_type)
|
|
||||||
VALUES ('SITES_ROOT', $1, 'string')
|
|
||||||
ON CONFLICT (config_key)
|
|
||||||
DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()",
|
|
||||||
)
|
|
||||||
.bind::<diesel::sql_types::Text, _>(&config.site_path)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.context("Failed to store SITES_ROOT")?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Configuration stored in database successfully with {} entries",
|
|
||||||
self.config_values.len() + 4
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_available_port(&self, preferred: u16) -> u16 {
|
|
||||||
if self.mode == InstallMode::Container {
|
|
||||||
return preferred;
|
|
||||||
}
|
|
||||||
|
|
||||||
for port in preferred..preferred + 100 {
|
|
||||||
if TcpListener::bind(("127.0.0.1", port)).is_ok() {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preferred
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_password(&self) -> String {
|
|
||||||
use rand::Rng;
|
|
||||||
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let mut rng = rand::rng();
|
|
||||||
(0..16)
|
|
||||||
.map(|_| {
|
|
||||||
let idx = rng.random_range(0..CHARSET.len());
|
|
||||||
CHARSET[idx] as char
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_service_host(&self, component: &str) -> String {
|
|
||||||
match self.mode {
|
|
||||||
InstallMode::Container => {
|
|
||||||
let container_name = format!("{}-{}", self.tenant, component);
|
|
||||||
self.get_container_ip(&container_name)
|
|
||||||
.unwrap_or_else(|_| "127.0.0.1".to_string())
|
|
||||||
}
|
|
||||||
InstallMode::Local => "127.0.0.1".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_container_ip(&self, container_name: &str) -> Result<String> {
|
|
||||||
let output = Command::new("lxc")
|
|
||||||
.args(&["list", container_name, "--format=json"])
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(anyhow::anyhow!("Failed to get container info"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let json: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
|
||||||
|
|
||||||
if let Some(ip) = json
|
|
||||||
.get(0)
|
|
||||||
.and_then(|c| c.get("state"))
|
|
||||||
.and_then(|s| s.get("network"))
|
|
||||||
.and_then(|n| n.get("eth0"))
|
|
||||||
.and_then(|e| e.get("addresses"))
|
|
||||||
.and_then(|a| a.get(0))
|
|
||||||
.and_then(|a| a.get("address"))
|
|
||||||
.and_then(|a| a.as_str())
|
|
||||||
{
|
|
||||||
Ok(ip.to_string())
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!("Could not extract container IP"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_service(&self, host: &str, port: u16, timeout_secs: u64) -> Result<()> {
|
|
||||||
info!(
|
|
||||||
"Waiting for service at {}:{} (timeout: {}s)",
|
|
||||||
host, port, timeout_secs
|
|
||||||
);
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
while start.elapsed().as_secs() < timeout_secs {
|
|
||||||
match TcpListener::bind((host, port)) {
|
|
||||||
Ok(_) => {
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
info!("Service {}:{} is ready", host, port);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"Timeout waiting for service at {}:{}",
|
|
||||||
host,
|
|
||||||
port
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
/// Application configuration - reads from database instead of .env
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub minio: DriveConfig,
|
pub minio: DriveConfig,
|
||||||
|
|
@ -18,7 +17,7 @@ pub struct AppConfig {
|
||||||
pub site_path: String,
|
pub site_path: String,
|
||||||
pub s3_bucket: String,
|
pub s3_bucket: String,
|
||||||
pub stack_path: PathBuf,
|
pub stack_path: PathBuf,
|
||||||
pub(crate) db_conn: Option<Arc<Mutex<PgConnection>>>,
|
pub db_conn: Option<Arc<Mutex<PgConnection>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -99,55 +98,40 @@ impl AppConfig {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get stack path for a specific component
|
|
||||||
pub fn component_path(&self, component: &str) -> PathBuf {
|
pub fn component_path(&self, component: &str) -> PathBuf {
|
||||||
self.stack_path.join(component)
|
self.stack_path.join(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get binary path for a component
|
|
||||||
pub fn bin_path(&self, component: &str) -> PathBuf {
|
pub fn bin_path(&self, component: &str) -> PathBuf {
|
||||||
self.stack_path.join("bin").join(component)
|
self.stack_path.join("bin").join(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get data path for a component
|
|
||||||
pub fn data_path(&self, component: &str) -> PathBuf {
|
pub fn data_path(&self, component: &str) -> PathBuf {
|
||||||
self.stack_path.join("data").join(component)
|
self.stack_path.join("data").join(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get config path for a component
|
|
||||||
pub fn config_path(&self, component: &str) -> PathBuf {
|
pub fn config_path(&self, component: &str) -> PathBuf {
|
||||||
self.stack_path.join("conf").join(component)
|
self.stack_path.join("conf").join(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get log path for a component
|
|
||||||
pub fn log_path(&self, component: &str) -> PathBuf {
|
pub fn log_path(&self, component: &str) -> PathBuf {
|
||||||
self.stack_path.join("logs").join(component)
|
self.stack_path.join("logs").join(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load configuration from database
|
|
||||||
/// Falls back to defaults if database is not yet initialized
|
|
||||||
pub fn from_database(conn: &mut PgConnection) -> Self {
|
pub fn from_database(conn: &mut PgConnection) -> Self {
|
||||||
info!("Loading configuration from database...");
|
info!("Loading configuration from database");
|
||||||
|
|
||||||
// Load all configuration from database
|
|
||||||
let config_map = match Self::load_config_from_db(conn) {
|
let config_map = match Self::load_config_from_db(conn) {
|
||||||
Ok(map) => {
|
Ok(map) => {
|
||||||
info!(
|
info!("Loaded {} config values from database", map.len());
|
||||||
"Successfully loaded {} config values from database",
|
|
||||||
map.len()
|
|
||||||
);
|
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(
|
warn!("Failed to load config from database: {}. Using defaults", e);
|
||||||
"Failed to load config from database: {}. Using defaults.",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to get config value with fallback
|
|
||||||
let get_str = |key: &str, default: &str| -> String {
|
let get_str = |key: &str, default: &str| -> String {
|
||||||
config_map
|
config_map
|
||||||
.get(key)
|
.get(key)
|
||||||
|
|
@ -234,10 +218,8 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy method - reads from .env for backward compatibility
|
|
||||||
/// Will be deprecated once database setup is complete
|
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
warn!("Loading configuration from environment variables (legacy mode)");
|
warn!("Loading configuration from environment variables");
|
||||||
|
|
||||||
let stack_path =
|
let stack_path =
|
||||||
std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string());
|
std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||||
|
|
@ -319,11 +301,9 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load all configuration from database into a HashMap
|
|
||||||
fn load_config_from_db(
|
fn load_config_from_db(
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> {
|
) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> {
|
||||||
// Try to query the server_configuration table
|
|
||||||
let results = diesel::sql_query(
|
let results = diesel::sql_query(
|
||||||
"SELECT id, config_key, config_value, config_type, is_encrypted
|
"SELECT id, config_key, config_value, config_type, is_encrypted
|
||||||
FROM server_configuration",
|
FROM server_configuration",
|
||||||
|
|
@ -338,7 +318,6 @@ impl AppConfig {
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a configuration value in the database
|
|
||||||
pub fn set_config(
|
pub fn set_config(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
|
|
@ -354,24 +333,20 @@ impl AppConfig {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a configuration value from the database
|
|
||||||
pub fn get_config(
|
pub fn get_config(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
key: &str,
|
key: &str,
|
||||||
fallback: Option<&str>,
|
fallback: Option<&str>,
|
||||||
) -> Result<String, diesel::result::Error> {
|
) -> Result<String, diesel::result::Error> {
|
||||||
// Use empty string when no fallback is supplied
|
|
||||||
let fallback_str = fallback.unwrap_or("");
|
let fallback_str = fallback.unwrap_or("");
|
||||||
|
|
||||||
// Define a temporary struct that matches the shape of the query result.
|
|
||||||
#[derive(Debug, QueryableByName)]
|
#[derive(Debug, QueryableByName)]
|
||||||
struct ConfigValue {
|
struct ConfigValue {
|
||||||
#[diesel(sql_type = Text)]
|
#[diesel(sql_type = Text)]
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the query and map the resulting row to the inner string.
|
|
||||||
let result = diesel::sql_query("SELECT get_config($1, $2) as value")
|
let result = diesel::sql_query("SELECT get_config($1, $2) as value")
|
||||||
.bind::<Text, _>(key)
|
.bind::<Text, _>(key)
|
||||||
.bind::<Text, _>(fallback_str)
|
.bind::<Text, _>(fallback_str)
|
||||||
|
|
@ -382,7 +357,6 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration manager for handling .gbot/config.csv files
|
|
||||||
pub struct ConfigManager {
|
pub struct ConfigManager {
|
||||||
conn: Arc<Mutex<PgConnection>>,
|
conn: Arc<Mutex<PgConnection>>,
|
||||||
}
|
}
|
||||||
|
|
@ -392,21 +366,17 @@ impl ConfigManager {
|
||||||
Self { conn }
|
Self { conn }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Watch and sync .gbot/config.csv file for a bot
|
|
||||||
pub fn sync_gbot_config(
|
pub fn sync_gbot_config(
|
||||||
&self,
|
&self,
|
||||||
bot_id: &uuid::Uuid,
|
bot_id: &uuid::Uuid,
|
||||||
config_path: &str,
|
config_path: &str,
|
||||||
) -> Result<usize, String> {
|
) -> Result<usize, String> {
|
||||||
// Import necessary crates for hashing and file handling
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
// Read the config.csv file
|
|
||||||
let content = fs::read_to_string(config_path)
|
let content = fs::read_to_string(config_path)
|
||||||
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||||
|
|
||||||
// Calculate file hash
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(content.as_bytes());
|
hasher.update(content.as_bytes());
|
||||||
let file_hash = format!("{:x}", hasher.finalize());
|
let file_hash = format!("{:x}", hasher.finalize());
|
||||||
|
|
@ -416,7 +386,6 @@ impl ConfigManager {
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| format!("Failed to acquire lock: {}", e))?;
|
.map_err(|e| format!("Failed to acquire lock: {}", e))?;
|
||||||
|
|
||||||
// Check if file has changed
|
|
||||||
#[derive(QueryableByName)]
|
#[derive(QueryableByName)]
|
||||||
struct SyncHash {
|
struct SyncHash {
|
||||||
#[diesel(sql_type = Text)]
|
#[diesel(sql_type = Text)]
|
||||||
|
|
@ -436,16 +405,13 @@ impl ConfigManager {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse CSV and update bot configuration
|
|
||||||
let mut updated = 0;
|
let mut updated = 0;
|
||||||
for line in content.lines().skip(1) {
|
for line in content.lines().skip(1) {
|
||||||
// Skip header
|
|
||||||
let parts: Vec<&str> = line.split(',').collect();
|
let parts: Vec<&str> = line.split(',').collect();
|
||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
let key = parts[0].trim();
|
let key = parts[0].trim();
|
||||||
let value = parts[1].trim();
|
let value = parts[1].trim();
|
||||||
|
|
||||||
// Insert or update bot configuration
|
|
||||||
diesel::sql_query(
|
diesel::sql_query(
|
||||||
"INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type)
|
"INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type)
|
||||||
VALUES (gen_random_uuid()::text, $1, $2, $3, 'string')
|
VALUES (gen_random_uuid()::text, $1, $2, $3, 'string')
|
||||||
|
|
@ -462,7 +428,6 @@ impl ConfigManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update sync record
|
|
||||||
diesel::sql_query(
|
diesel::sql_query(
|
||||||
"INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count)
|
"INSERT INTO gbot_config_sync (id, bot_id, config_file_path, file_hash, sync_count)
|
||||||
VALUES (gen_random_uuid()::text, $1, $2, $3, 1)
|
VALUES (gen_random_uuid()::text, $1, $2, $3, 1)
|
||||||
|
|
@ -476,10 +441,7 @@ impl ConfigManager {
|
||||||
.execute(&mut *conn)
|
.execute(&mut *conn)
|
||||||
.map_err(|e| format!("Failed to update sync record: {}", e))?;
|
.map_err(|e| format!("Failed to update sync record: {}", e))?;
|
||||||
|
|
||||||
info!(
|
info!("Synced {} config values for bot {} from {}", updated, bot_id, config_path);
|
||||||
"Synced {} config values for bot {} from {}",
|
|
||||||
updated, bot_id, config_path
|
|
||||||
);
|
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
src/main.rs
108
src/main.rs
|
|
@ -35,15 +35,19 @@ mod whatsapp;
|
||||||
|
|
||||||
use crate::auth::auth_handler;
|
use crate::auth::auth_handler;
|
||||||
use crate::automation::AutomationService;
|
use crate::automation::AutomationService;
|
||||||
use crate::bot::{start_session, websocket_handler};
|
|
||||||
use crate::bootstrap::BootstrapManager;
|
use crate::bootstrap::BootstrapManager;
|
||||||
|
use crate::bot::{start_session, websocket_handler};
|
||||||
use crate::channels::{VoiceAdapter, WebChannelAdapter};
|
use crate::channels::{VoiceAdapter, WebChannelAdapter};
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::drive_monitor::DriveMonitor;
|
use crate::drive_monitor::DriveMonitor;
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
use crate::email::{get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email};
|
use crate::email::{
|
||||||
|
get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email,
|
||||||
|
};
|
||||||
use crate::file::{init_drive, upload_file};
|
use crate::file::{init_drive, upload_file};
|
||||||
use crate::llm_legacy::llm_local::{chat_completions_local, embeddings_local, ensure_llama_servers_running};
|
use crate::llm_legacy::llm_local::{
|
||||||
|
chat_completions_local, embeddings_local, ensure_llama_servers_running,
|
||||||
|
};
|
||||||
use crate::meet::{voice_start, voice_stop};
|
use crate::meet::{voice_start, voice_stop};
|
||||||
use crate::package_manager::InstallMode;
|
use crate::package_manager::InstallMode;
|
||||||
use crate::session::{create_session, get_session_history, get_sessions};
|
use crate::session::{create_session, get_session_history, get_sessions};
|
||||||
|
|
@ -55,23 +59,29 @@ use crate::whatsapp::WhatsAppAdapter;
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"install" | "remove" | "list" | "status" | "--help" | "-h" => {
|
"install" | "remove" | "list" | "status" | "--help" | "-h" => {
|
||||||
match package_manager::cli::run() {
|
match package_manager::cli::run().await {
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => return Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("CLI error: {}", e);
|
eprintln!("CLI error: {}", e);
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("CLI command failed: {}", e)));
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("CLI command failed: {}", e),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown command: {}", command);
|
eprintln!("Unknown command: {}", command);
|
||||||
eprintln!("Run 'botserver --help' for usage information");
|
eprintln!("Run 'botserver --help' for usage information");
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Unknown command: {}", command)));
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
format!("Unknown command: {}", command),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,13 +90,13 @@ async fn main() -> std::io::Result<()> {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
info!("Starting BotServer bootstrap process");
|
info!("Starting BotServer bootstrap process");
|
||||||
|
|
||||||
let install_mode = if args.contains(&"--container".to_string()) {
|
let install_mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
} else {
|
} else {
|
||||||
InstallMode::Local
|
InstallMode::Local
|
||||||
};
|
};
|
||||||
|
|
||||||
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
||||||
args.get(idx + 1).cloned()
|
args.get(idx + 1).cloned()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -102,7 +112,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Bootstrap failed: {}", e);
|
log::error!("Bootstrap failed: {}", e);
|
||||||
info!("Attempting to load configuration from database");
|
info!("Attempting to load configuration from database");
|
||||||
match diesel::Connection::establish(&format!("postgres://localhost:5432/botserver_db")) {
|
match diesel::Connection::establish(&format!("postgres://localhost:5432/botserver_db"))
|
||||||
|
{
|
||||||
Ok(mut conn) => AppConfig::from_database(&mut conn),
|
Ok(mut conn) => AppConfig::from_database(&mut conn),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("Database not available, using environment variables as fallback");
|
info!("Database not available, using environment variables as fallback");
|
||||||
|
|
@ -113,22 +124,31 @@ async fn main() -> std::io::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = std::sync::Arc::new(cfg.clone());
|
let config = std::sync::Arc::new(cfg.clone());
|
||||||
|
|
||||||
info!("Establishing database connection to {}", cfg.database_url());
|
info!("Establishing database connection to {}", cfg.database_url());
|
||||||
let db_pool = match diesel::Connection::establish(&cfg.database_url()) {
|
let db_pool = match diesel::Connection::establish(&cfg.database_url()) {
|
||||||
Ok(conn) => Arc::new(Mutex::new(conn)),
|
Ok(conn) => Arc::new(Mutex::new(conn)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to connect to main database: {}", e);
|
log::error!("Failed to connect to main database: {}", e);
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, format!("Database connection failed: {}", e)));
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::ConnectionRefused,
|
||||||
|
format!("Database connection failed: {}", e),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let db_custom_pool = db_pool.clone();
|
let db_custom_pool = db_pool.clone();
|
||||||
|
|
||||||
info!("Initializing LLM server at {}", cfg.ai.endpoint);
|
info!("Initializing LLM server at {}", cfg.ai.endpoint);
|
||||||
ensure_llama_servers_running().await.expect("Failed to initialize LLM local server");
|
ensure_llama_servers_running()
|
||||||
|
.await
|
||||||
|
.expect("Failed to initialize LLM local server");
|
||||||
|
|
||||||
let cache_url = cfg.config_path("cache").join("redis.conf").display().to_string();
|
let cache_url = cfg
|
||||||
|
.config_path("cache")
|
||||||
|
.join("redis.conf")
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
||||||
Ok(client) => Some(Arc::new(client)),
|
Ok(client) => Some(Arc::new(client)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -138,18 +158,37 @@ async fn main() -> std::io::Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let tool_manager = Arc::new(tools::ToolManager::new());
|
let tool_manager = Arc::new(tools::ToolManager::new());
|
||||||
let llm_provider = Arc::new(crate::llm::OpenAIClient::new("empty".to_string(), Some(cfg.ai.endpoint.clone())));
|
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
|
||||||
|
"empty".to_string(),
|
||||||
|
Some(cfg.ai.endpoint.clone()),
|
||||||
|
));
|
||||||
|
|
||||||
let web_adapter = Arc::new(WebChannelAdapter::new());
|
let web_adapter = Arc::new(WebChannelAdapter::new());
|
||||||
let voice_adapter = Arc::new(VoiceAdapter::new("https://livekit.example.com".to_string(), "api_key".to_string(), "api_secret".to_string()));
|
let voice_adapter = Arc::new(VoiceAdapter::new(
|
||||||
let whatsapp_adapter = Arc::new(WhatsAppAdapter::new("whatsapp_token".to_string(), "phone_number_id".to_string(), "verify_token".to_string()));
|
"https://livekit.example.com".to_string(),
|
||||||
|
"api_key".to_string(),
|
||||||
|
"api_secret".to_string(),
|
||||||
|
));
|
||||||
|
let whatsapp_adapter = Arc::new(WhatsAppAdapter::new(
|
||||||
|
"whatsapp_token".to_string(),
|
||||||
|
"phone_number_id".to_string(),
|
||||||
|
"verify_token".to_string(),
|
||||||
|
));
|
||||||
let tool_api = Arc::new(tools::ToolApi::new());
|
let tool_api = Arc::new(tools::ToolApi::new());
|
||||||
|
|
||||||
info!("Initializing MinIO drive at {}", cfg.minio.server);
|
info!("Initializing MinIO drive at {}", cfg.minio.server);
|
||||||
let drive = init_drive(&config.minio).await.expect("Failed to initialize Drive");
|
let drive = init_drive(&config.minio)
|
||||||
|
.await
|
||||||
|
.expect("Failed to initialize Drive");
|
||||||
|
|
||||||
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(diesel::Connection::establish(&cfg.database_url()).unwrap(), redis_client.clone())));
|
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
|
||||||
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new(diesel::Connection::establish(&cfg.database_url()).unwrap(), redis_client.clone())));
|
diesel::Connection::establish(&cfg.database_url()).unwrap(),
|
||||||
|
redis_client.clone(),
|
||||||
|
)));
|
||||||
|
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new(
|
||||||
|
diesel::Connection::establish(&cfg.database_url()).unwrap(),
|
||||||
|
redis_client.clone(),
|
||||||
|
)));
|
||||||
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
s3_client: Some(drive.clone()),
|
s3_client: Some(drive.clone()),
|
||||||
|
|
@ -163,7 +202,10 @@ async fn main() -> std::io::Result<()> {
|
||||||
auth_service: auth_service.clone(),
|
auth_service: auth_service.clone(),
|
||||||
channels: Arc::new(Mutex::new({
|
channels: Arc::new(Mutex::new({
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert("web".to_string(), web_adapter.clone() as Arc<dyn crate::channels::ChannelAdapter>);
|
map.insert(
|
||||||
|
"web".to_string(),
|
||||||
|
web_adapter.clone() as Arc<dyn crate::channels::ChannelAdapter>,
|
||||||
|
);
|
||||||
map
|
map
|
||||||
})),
|
})),
|
||||||
response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
response_channels: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
||||||
|
|
@ -173,12 +215,20 @@ async fn main() -> std::io::Result<()> {
|
||||||
tool_api: tool_api.clone(),
|
tool_api: tool_api.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Starting HTTP server on {}:{}", config.server.host, config.server.port);
|
info!(
|
||||||
|
"Starting HTTP server on {}:{}",
|
||||||
|
config.server.host, config.server.port
|
||||||
|
);
|
||||||
|
|
||||||
let worker_count = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
|
let worker_count = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(4);
|
||||||
|
|
||||||
let automation_state = app_state.clone();
|
let automation_state = app_state.clone();
|
||||||
let automation = AutomationService::new(automation_state, "templates/announcements.gbai/announcements.gbdialog");
|
let automation = AutomationService::new(
|
||||||
|
automation_state,
|
||||||
|
"templates/announcements.gbai/announcements.gbdialog",
|
||||||
|
);
|
||||||
let _automation_handle = automation.spawn();
|
let _automation_handle = automation.spawn();
|
||||||
|
|
||||||
let drive_state = app_state.clone();
|
let drive_state = app_state.clone();
|
||||||
|
|
@ -187,9 +237,13 @@ async fn main() -> std::io::Result<()> {
|
||||||
let _drive_handle = drive_monitor.spawn();
|
let _drive_handle = drive_monitor.spawn();
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let cors = Cors::default().allow_any_origin().allow_any_method().allow_any_header().max_age(3600);
|
let cors = Cors::default()
|
||||||
|
.allow_any_origin()
|
||||||
|
.allow_any_method()
|
||||||
|
.allow_any_header()
|
||||||
|
.max_age(3600);
|
||||||
let app_state_clone = app_state.clone();
|
let app_state_clone = app_state.clone();
|
||||||
|
|
||||||
let mut app = App::new()
|
let mut app = App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue