mod consumer;
mod storage;
mod bloom;
mod producer;
pub mod search;
pub mod legal;
pub mod scheduler;
use anyhow::Result;
use bgustscraper::worker::{PlaywrightWorker, ScrapingEngine, ScrapeOptions};
use crate::storage::StorageManager;
use crate::bloom::ThreadSafeBloomFilter;
use crate::producer::Producer;
use uuid::Uuid;
use sha2::{Sha256, Digest};
use std::sync::OnceLock;
use serde_json::json;
fn bloom_filter() -> &'static ThreadSafeBloomFilter {
static BLOOM: OnceLock<ThreadSafeBloomFilter> = OnceLock::new();
BLOOM.get_or_init(|| ThreadSafeBloomFilter::new(100_000, 5))
}
pub async fn run_crawler(
url: &str,
deep_scan: bool,
client_id: Option<&str>,
project_id: Option<&str>,
domain: Option<&str>,
) -> Result<String> {
let bf = bloom_filter();
if bf.contains(&url.to_string()) {
println!("🚫 [CRAWLER] URL already visited (deduplicated by Bloom Filter): {}", url);
return Ok("DUPLICATE_SKIPPED".to_string());
}
bf.insert(&url.to_string());
println!("🚀 [CRAWLER] Crawling URL: {}", url);
let mut worker = PlaywrightWorker::new(true, ScrapingEngine::Playwright)?;
let storage = StorageManager::new().await?;
let response = worker.scrape(url, Some(ScrapeOptions {
deep_scan,
semantic_embeddings: None,
text: None,
prompt: None,
}))?;
if response.status == "success" {
let markdown = response.markdown.unwrap_or_else(|| "No markdown available".to_string());
let id = Uuid::new_v4().to_string();
let mut hasher = Sha256::new();
hasher.update(markdown.as_bytes());
let sha256_bytes = hasher.finalize();
let sha256_hash = sha256_bytes.iter().map(|b| format!("{:02x}", b)).collect::<String>();
let pointer = storage.save_object(&id, markdown.as_bytes().to_vec(), "text/markdown").await?;
println!("📥 [CRAWLER] Saved document to object store at {}", pointer);
match Producer::new("localhost:9094") {
Ok(producer) => {
let payload = json!({
"id": id,
"client_id": client_id.unwrap_or("client_default"),
"project_id": project_id.unwrap_or("project_default"),
"domain": domain.unwrap_or("general"),
"url": url,
"pointer": pointer,
"sha256": sha256_hash,
"content": markdown
});
match producer.send_message("crawl_results", &id, &payload.to_string()).await {
Ok(_) => println!("📢 [CRAWLER] Published crawl results to Redpanda for URL: {}", url),
Err(e) => eprintln!("⚠️ [CRAWLER] Failed to publish message to Redpanda: {:?}", e),
}
}
Err(e) => {
eprintln!("⚠️ [CRAWLER] Failed to initialize Kafka producer: {:?}", e);
}
}
Ok(pointer)
} else {
Err(anyhow::anyhow!("Crawler failed: {}", response.message.unwrap_or_default()))
}
}
pub async fn start_background_consumer(brokers: &str) -> Result<()> {
let consumer = consumer::Consumer::new(brokers, "crawler_group", &["crawl_tasks"]);
consumer.listen(|payload| async move {
println!("Received background task: {}", payload);
if let Ok(req) = serde_json::from_str::<serde_json::Value>(&payload) {
if let Some(url) = req["url"].as_str() {
let client_id = req["client_id"].as_str();
let project_id = req["project_id"].as_str();
let domain = req["domain"].as_str();
let deep_scan = req["deep_scan"].as_bool().unwrap_or(false);
let _ = run_crawler(url, deep_scan, client_id, project_id, domain).await;
}
}
}).await
}