use clap::{Arg, Command};
use flate2::read::GzDecoder;
use reqwest::{header, Client};
use serde::{Deserialize, Serialize};
use std::env;
use std::error::Error;
use std::io::Read;
#[derive(Debug, Deserialize)]
struct BraveSearchResponse {
#[serde(default)]
web: Web,
}
#[derive(Debug, Deserialize, Default)]
struct Web {
#[serde(default)]
results: Vec<WebResult>,
}
#[derive(Debug, Deserialize)]
struct WebResult {
title: String,
url: String,
description: String,
}
#[derive(Debug, Serialize)]
struct CollectedUrl {
title: String,
url: String,
description: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let matches = Command::new("Brave Search CLI")
.version("0.1.0")
.author("Shane Witbeck <shane@surly.dev>")
.about("Search the web using the Brave Search API")
.arg(
Arg::new("query")
.short('q')
.long("query")
.value_name("QUERY")
.help("The query to search for")
.required(true),
)
.arg(
Arg::new("count")
.short('c')
.long("count")
.value_name("COUNT")
.help("Number of results to retrieve (default: 10)")
.default_value("10"),
)
.get_matches();
let query = matches.get_one::<String>("query").unwrap();
let api_key = env::var("BRAVE_API_KEY").expect("BRAVE_API_KEY environment variable not set");
let count = matches.get_one::<String>("count").unwrap().parse::<u32>().unwrap_or(10);
let mut headers = header::HeaderMap::new();
headers.insert(header::ACCEPT, header::HeaderValue::from_static("application/json"));
headers.insert(header::ACCEPT_ENCODING, header::HeaderValue::from_static("gzip"));
headers.insert(
"X-Subscription-Token",
header::HeaderValue::from_str(&api_key)?
);
let http_client = Client::builder()
.default_headers(headers)
.build()?;
let url = format!(
"https://api.search.brave.com/res/v1/web/search?q={}&count={}",
urlencoding::encode(query),
count
);
let response = http_client.get(&url).send().await?;
if !response.status().is_success() {
return Err(format!("API request failed with status: {}", response.status()).into());
}
let response_bytes = response.bytes().await?;
if response_bytes.is_empty() {
return Err("Empty response from API".into());
}
let is_gzip = response_bytes.len() >= 2 && response_bytes[0] == 0x1f && response_bytes[1] == 0x8b;
let search_response: BraveSearchResponse = if is_gzip {
let mut decoder = GzDecoder::new(&response_bytes[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)
.map_err(|e| format!("Failed to decompress gzip data: {}", e))?;
serde_json::from_slice(&decompressed)
.map_err(|e| format!("Failed to parse decompressed JSON: {}", e))?
} else {
serde_json::from_slice(&response_bytes)
.map_err(|e| format!("Failed to parse API response: {}", e))?
};
let collected_urls: Vec<CollectedUrl> = search_response.web.results
.into_iter()
.map(|result| CollectedUrl {
title: result.title,
url: result.url,
description: result.description,
})
.collect();
let results = serde_json::json!({
"results": collected_urls
});
let json = serde_json::to_string_pretty(&results)?;
println!("{}", json);
Ok(())
}