This commit is contained in:
parent
e15da79204
commit
50bfad7642
11 changed files with 311 additions and 92 deletions
|
@ -5,19 +5,21 @@ FOR EACH item IN items
|
|||
let website = WEBSITE OF item.company
|
||||
PRINT website
|
||||
|
||||
WAIT 10
|
||||
|
||||
let page = GET website
|
||||
|
||||
let prompt = "Create a website for " + item.company + " with the following details: " + page
|
||||
let prompt = "Build the same simulator , but for " + item.company + " using just *content about the company* from its website, so it is possible to create a good and useful emulator in the same langue as the content: " + page
|
||||
|
||||
let alias = LLM "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", item.company, 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"
|
||||
let subject = "General Bots"
|
||||
let body = "Oi, tudo bem? Criamos o simulador " + alias + " especificamente para vocês!" + "\n\n Acesse o site: https://sites.pragmatismo.com.br/" + alias + "\n\n" + "Para acessar o simulador, clique no link acima ou copie e cole no seu navegador." + "\n\n" + "Para iniciar, clique no ícone de Play." + "\n\n" + "Atenciosamente,\nDário Vieira"
|
||||
|
||||
CREATE_DRAFT to, subject, body
|
||||
|
||||
NEXT item
|
||||
|
||||
|
||||
NEXT item
|
|
@ -8,6 +8,7 @@ pub struct AppConfig {
|
|||
pub database_custom: DatabaseConfig,
|
||||
pub email: EmailConfig,
|
||||
pub ai: AIConfig,
|
||||
pub site_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -141,6 +142,7 @@ impl AppConfig {
|
|||
database_custom,
|
||||
email,
|
||||
ai,
|
||||
site_path: env::var("SITES_ROOT").unwrap()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -289,6 +289,83 @@ pub async fn get_latest_email_from(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_latest_sent_to(
|
||||
email_config: &EmailConfig,
|
||||
to_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,
|
||||
)?;
|
||||
|
||||
// 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("Sent").is_err() {
|
||||
session.select("Sent Items")?;
|
||||
}
|
||||
|
||||
// Search for emails from the specified sender
|
||||
let search_query = format!("TO \"{}\"", to_email);
|
||||
let messages = session.search(&search_query)?;
|
||||
|
||||
if messages.is_empty() {
|
||||
session.logout()?;
|
||||
return Err(format!("No emails found to {}", to_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)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_latest_email_from_sender(
|
||||
email_config: &EmailConfig,
|
||||
from_email: &str,
|
||||
|
@ -366,6 +443,7 @@ pub async fn fetch_latest_email_from_sender(
|
|||
}
|
||||
|
||||
|
||||
|
||||
#[actix_web::post("/emails/send")]
|
||||
pub async fn send_email(
|
||||
payload: web::Json<(String, String, String)>,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::services::email::SaveDraftRequest;
|
||||
use crate::services::email::{fetch_latest_email_from_sender, save_email_draft};
|
||||
use crate::services::email::{fetch_latest_sent_to, SaveDraftRequest};
|
||||
use crate::services::email::{save_email_draft};
|
||||
use crate::services::state::AppState;
|
||||
use rhai::Dynamic;
|
||||
use rhai::Engine;
|
||||
|
@ -35,10 +35,10 @@ async fn execute_create_draft(
|
|||
subject: &str,
|
||||
reply_text: &str,
|
||||
) -> Result<String, String> {
|
||||
let get_result = fetch_latest_email_from_sender(&state.config.clone().unwrap().email, to).await;
|
||||
let get_result = fetch_latest_sent_to(&state.config.clone().unwrap().email, to).await;
|
||||
let email_body = if let Ok(get_result_str) = get_result {
|
||||
if !get_result_str.is_empty() {
|
||||
get_result_str + reply_text
|
||||
reply_text.to_string() + get_result_str.as_str()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ use rhai::Dynamic;
|
|||
use rhai::Engine;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{ PathBuf};
|
||||
use std::io::Read;
|
||||
|
||||
use crate::services::state::AppState;
|
||||
use crate::services::utils;
|
||||
|
@ -12,26 +13,25 @@ pub fn create_site_keyword(state: &AppState, engine: &mut Engine) {
|
|||
engine
|
||||
.register_custom_syntax(
|
||||
&[
|
||||
"CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",",
|
||||
"$expr$",
|
||||
"CREATE_SITE", "$expr$", ",", "$expr$", ",", "$expr$",
|
||||
|
||||
],
|
||||
true, // Statement
|
||||
true,
|
||||
move |context, inputs| {
|
||||
if inputs.len() < 5 {
|
||||
if inputs.len() < 3 {
|
||||
return Err("Not enough arguments for CREATE SITE".into());
|
||||
}
|
||||
|
||||
let _name = context.eval_expression_tree(&inputs[0])?;
|
||||
|
||||
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 ai_config = state_clone.config.as_ref().expect("Config must be initialized").ai.clone();
|
||||
// Use the same pattern as find_keyword
|
||||
let fut = create_site(&ai_config, _name, prompt);
|
||||
let alias = context.eval_expression_tree(&inputs[0])?;
|
||||
let template_dir = context.eval_expression_tree(&inputs[1])?;
|
||||
let prompt = context.eval_expression_tree(&inputs[2])?;
|
||||
|
||||
let config = state_clone.config.as_ref().expect("Config must be initialized").clone();
|
||||
|
||||
let fut = create_site(&config, alias, template_dir, prompt);
|
||||
let result =
|
||||
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
|
||||
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
||||
.map_err(|e| format!("Site creation failed: {}", e))?;
|
||||
|
||||
Ok(Dynamic::from(result))
|
||||
},
|
||||
|
@ -40,26 +40,51 @@ pub fn create_site_keyword(state: &AppState, engine: &mut Engine) {
|
|||
}
|
||||
|
||||
async fn create_site(
|
||||
ai_config: &crate::services::config::AIConfig,
|
||||
_name: Dynamic,
|
||||
config: &crate::services::config::AppConfig,
|
||||
alias: Dynamic,
|
||||
template_dir: Dynamic,
|
||||
prompt: Dynamic,
|
||||
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
// Convert paths to platform-specific format
|
||||
let base_path = PathBuf::from(&config.site_path);
|
||||
let template_path = base_path.join(template_dir.to_string());
|
||||
let alias_path = base_path.join(alias.to_string());
|
||||
|
||||
// Call the LLM to generate the HTML contents
|
||||
let llm_result = utils::call_llm(&prompt.to_string(), &ai_config).await?;
|
||||
// Create destination directory
|
||||
fs::create_dir_all(&alias_path).map_err(|e| e.to_string())?;
|
||||
|
||||
// Create the directory structure
|
||||
let base_path = "/opt/gbo/tenants/pragmatismo/proxy/data/websites/sites.pragmatismo.com.br";
|
||||
let site_name = format!("{}", _name.to_string());
|
||||
let full_path = format!("{}/{}", base_path, site_name);
|
||||
// Process all HTML files in template directory
|
||||
let mut combined_content = String::new();
|
||||
|
||||
for entry in fs::read_dir(&template_path).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "html") {
|
||||
let mut file = fs::File::open(&path).map_err(|e| e.to_string())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).map_err(|e| e.to_string())?;
|
||||
|
||||
combined_content.push_str(&contents);
|
||||
combined_content.push_str("\n\n--- TEMPLATE SEPARATOR ---\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
fs::create_dir_all(&full_path).map_err(|e| e.to_string())?;
|
||||
// Combine template content with prompt
|
||||
let full_prompt = format!(
|
||||
"TEMPLATE FILES:\n{}\n\nPROMPT: {}\n\nGenerate a new HTML file cloning all previous TEMPLATE (keeping only the local _assets libraries use, no external resources), but turning this into this prompt:",
|
||||
combined_content,
|
||||
prompt.to_string()
|
||||
);
|
||||
|
||||
// Write the HTML file
|
||||
let index_path = Path::new(&full_path).join("index.html");
|
||||
// Call LLM with the combined prompt
|
||||
println!("Asking LLM to create site.");
|
||||
let llm_result = utils::call_llm(&full_prompt, &config.ai).await?;
|
||||
|
||||
// Write the generated HTML file
|
||||
let index_path = alias_path.join("index.html");
|
||||
fs::write(index_path, llm_result).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("Site created at: {}", full_path);
|
||||
Ok(full_path)
|
||||
}
|
||||
println!("Site created at: {}", alias_path.display());
|
||||
Ok(alias_path.to_string_lossy().into_owned())
|
||||
}
|
|
@ -2,7 +2,6 @@ use rhai::Dynamic;
|
|||
use rhai::Engine;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{PgPool};
|
||||
use std::error::Error;
|
||||
|
||||
use crate::services::state::AppState;
|
||||
use crate::services::utils;
|
||||
|
@ -54,7 +53,7 @@ pub async fn execute_find(
|
|||
table_str, filter_str
|
||||
);
|
||||
|
||||
let (where_clause, params) = parse_filter(filter_str).map_err(|e| e.to_string())?;
|
||||
let (where_clause, params) = utils::parse_filter(filter_str).map_err(|e| e.to_string())?;
|
||||
|
||||
let query = format!(
|
||||
"SELECT * FROM {} WHERE {} LIMIT 10",
|
||||
|
@ -87,24 +86,3 @@ pub async fn execute_find(
|
|||
}))
|
||||
}
|
||||
|
||||
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
||||
fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||
let parts: Vec<&str> = filter_str.split('=').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Invalid filter format. Expected 'KEY=VALUE'".into());
|
||||
}
|
||||
|
||||
let column = parts[0].trim();
|
||||
let value = parts[1].trim();
|
||||
|
||||
// Validate column name to prevent SQL injection
|
||||
if !column
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
{
|
||||
return Err("Invalid column name in filter".into());
|
||||
}
|
||||
|
||||
// Return the parameterized query part and the value separately
|
||||
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
use reqwest;
|
||||
use crate::services::state::AppState;
|
||||
use std::error::Error;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
|
||||
pub fn get_keyword(_state: &AppState, engine: &mut Engine) {
|
||||
engine.register_custom_syntax(
|
||||
|
@ -29,7 +31,7 @@ pub fn get_keyword(_state: &AppState, engine: &mut Engine) {
|
|||
).unwrap();
|
||||
}
|
||||
|
||||
pub async fn execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
pub async fn _execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
println!("Starting execute_get with URL: {}", url);
|
||||
|
||||
let response = reqwest::get(url).await?;
|
||||
|
@ -37,4 +39,28 @@ pub async fn execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Syn
|
|||
|
||||
println!("GET request successful, got {} bytes", content.len());
|
||||
Ok(format!("Secure content fetched: {}", content))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_get(url: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||
println!("Starting execute_get with URL: {}", url);
|
||||
|
||||
let response = reqwest::get(url).await?;
|
||||
let html_content = response.text().await?;
|
||||
|
||||
// Parse HTML and extract text
|
||||
let document = Html::parse_document(&html_content);
|
||||
let selector = Selector::parse("body").unwrap(); // Focus on body content
|
||||
let body = document.select(&selector).next().unwrap();
|
||||
let text_content = body.text().collect::<Vec<_>>().join(" ");
|
||||
|
||||
// Clean up the text (remove extra whitespace, newlines, etc.)
|
||||
let cleaned_text = text_content
|
||||
.replace('\n', " ")
|
||||
.replace('\t', " ")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
println!("GET request successful, extracted {} characters of text", cleaned_text.len());
|
||||
Ok(cleaned_text)
|
||||
}
|
||||
|
|
|
@ -77,7 +77,8 @@ async fn perform_search(
|
|||
|
||||
// Extract results
|
||||
let results = extract_search_results(&driver).await?;
|
||||
|
||||
driver.quit().await?;
|
||||
|
||||
if !results.is_empty() {
|
||||
Ok(results[0].clone())
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ pub fn llm_keyword(state: &AppState, engine: &mut Engine) {
|
|||
let ai_config = state.config.clone().unwrap().ai.clone();
|
||||
|
||||
engine.register_custom_syntax(
|
||||
&["LLM", "$string$"], // Syntax: LLM "text to process"
|
||||
&["LLM", "$expr$"], // Syntax: LLM "text to process"
|
||||
false, // Expression, not statement
|
||||
move |context, inputs| {
|
||||
let text = context.eval_expression_tree(&inputs[0])?;
|
||||
|
@ -16,7 +16,8 @@ pub fn llm_keyword(state: &AppState, engine: &mut Engine) {
|
|||
|
||||
// Use the same pattern as GET
|
||||
|
||||
let fut = call_llm(&text_str, &ai_config);
|
||||
let fut = call_llm(
|
||||
&text_str, &ai_config);
|
||||
let result = tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(fut)
|
||||
}).map_err(|e| format!("LLM call failed: {}", e))?;
|
||||
|
|
|
@ -1,36 +1,120 @@
|
|||
use rhai::Dynamic;
|
||||
use rhai::Engine;
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::{PgPool};
|
||||
use std::error::Error;
|
||||
|
||||
use crate::services::state::AppState;
|
||||
use crate::services::utils;
|
||||
|
||||
|
||||
pub fn set_keyword(_state: &AppState, engine: &mut Engine) {
|
||||
pub fn set_keyword(state: &AppState, engine: &mut Engine) {
|
||||
let db = state.db_custom.clone();
|
||||
|
||||
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])?;
|
||||
.register_custom_syntax(&["SET", "$expr$", ",", "$expr$", ",", "$expr$"], false, {
|
||||
let db = db.clone();
|
||||
|
||||
let table_str = table_name.to_string();
|
||||
let key_str = key_value.to_string();
|
||||
let value_str = value.to_string();
|
||||
move |context, inputs| {
|
||||
let table_name = context.eval_expression_tree(&inputs[0])?;
|
||||
let filter = context.eval_expression_tree(&inputs[1])?;
|
||||
let updates = context.eval_expression_tree(&inputs[2])?;
|
||||
let binding = db.as_ref().unwrap();
|
||||
|
||||
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();
|
||||
// Use the current async context instead of creating a new runtime
|
||||
let binding2 = table_name.to_string();
|
||||
let binding3 = filter.to_string();
|
||||
let binding4 = updates.to_string();
|
||||
let fut = execute_set(binding, &binding2, &binding3, &binding4);
|
||||
|
||||
// Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on
|
||||
let result =
|
||||
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
|
||||
.map_err(|e| format!("DB error: {}", e))?;
|
||||
|
||||
if let Some(rows_affected) = result.get("rows_affected") {
|
||||
Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0)))
|
||||
} else {
|
||||
Err("No rows affected".into())
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn execute_set(
|
||||
pool: &PgPool,
|
||||
table_str: &str,
|
||||
filter_str: &str,
|
||||
updates_str: &str,
|
||||
) -> Result<Value, String> {
|
||||
println!(
|
||||
"Starting execute_set with table: {}, filter: {}, updates: {}",
|
||||
table_str, filter_str, updates_str
|
||||
);
|
||||
|
||||
// Parse the filter condition
|
||||
let (where_clause, filter_params) = utils::parse_filter(filter_str).map_err(|e| e.to_string())?;
|
||||
|
||||
// Parse the updates
|
||||
let (set_clause, update_params) = parse_updates(updates_str).map_err(|e| e.to_string())?;
|
||||
|
||||
// Combine all parameters (updates first, then filter)
|
||||
let mut params = update_params;
|
||||
params.extend(filter_params);
|
||||
|
||||
let query = format!(
|
||||
"UPDATE {} SET {} WHERE {}",
|
||||
table_str, set_clause, where_clause
|
||||
);
|
||||
println!("Executing query: {}", query);
|
||||
|
||||
// Execute the update
|
||||
let result = sqlx::query(&query)
|
||||
.bind(¶ms[0]) // First update value
|
||||
.bind(¶ms[1]) // Second update value if exists
|
||||
.bind(¶ms[2]) // Filter value
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("SQL execution error: {}", e);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
println!("Update successful, affected {} rows", result.rows_affected());
|
||||
|
||||
Ok(json!({
|
||||
"command": "set",
|
||||
"table": table_str,
|
||||
"filter": filter_str,
|
||||
"updates": updates_str,
|
||||
"rows_affected": result.rows_affected()
|
||||
}))
|
||||
}
|
||||
|
||||
// Helper function to parse the updates string into SQL SET clause and parameters
|
||||
fn parse_updates(updates_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||
let mut set_clauses = Vec::new();
|
||||
let mut params = Vec::new();
|
||||
|
||||
// Split multiple updates by comma
|
||||
for update in updates_str.split(',') {
|
||||
let parts: Vec<&str> = update.split('=').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Invalid update format. Expected 'KEY=VALUE'".into());
|
||||
}
|
||||
|
||||
let column = parts[0].trim();
|
||||
let value = parts[1].trim();
|
||||
|
||||
// Validate column name to prevent SQL injection
|
||||
if !column.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
return Err("Invalid column name in update".into());
|
||||
}
|
||||
|
||||
set_clauses.push(format!("{} = ${}", column, set_clauses.len() + 1));
|
||||
params.push(value.to_string());
|
||||
}
|
||||
|
||||
Ok((set_clauses.join(", "), params))
|
||||
}
|
|
@ -216,3 +216,25 @@ pub async fn download_file(url: &str, output_path: &str) -> Result<(), Box<dyn s
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
||||
pub fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||
let parts: Vec<&str> = filter_str.split('=').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err("Invalid filter format. Expected 'KEY=VALUE'".into());
|
||||
}
|
||||
|
||||
let column = parts[0].trim();
|
||||
let value = parts[1].trim();
|
||||
|
||||
// Validate column name to prevent SQL injection
|
||||
if !column
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
{
|
||||
return Err("Invalid column name in filter".into());
|
||||
}
|
||||
|
||||
// Return the parameterized query part and the value separately
|
||||
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue