use crate::api::client::GdeltClient;
use crate::api::doc::{DocApi, DocSearchParams, DocTimelineParams, DocWordcloudParams};
use crate::cli::args::{DocCommand, GlobalArgs, SortOrder, TimelineMode};
use crate::cli::output::OutputWriter;
use crate::enrich::{ArticleInput, EnrichmentService};
use crate::error::{ExitStatus, Result};
use std::time::Duration;
pub async fn handle_doc(cmd: DocCommand, global: &GlobalArgs) -> Result<ExitStatus> {
let client = GdeltClient::with_timeout(Duration::from_secs(global.timeout))?;
let api = DocApi::new(client.clone());
match cmd {
DocCommand::Search(args) => {
let sort = match args.sort {
SortOrder::DateDesc => "datedesc",
SortOrder::DateAsc => "dateasc",
SortOrder::ToneDesc => "tonedesc",
SortOrder::ToneAsc => "toneasc",
SortOrder::HybridRel => "hybridrel",
};
let params = DocSearchParams {
query: args.query,
timespan: Some(args.timespan),
start_datetime: args.start,
end_datetime: args.end,
max_records: Some(args.max_records),
sort: Some(sort.to_string()),
source_lang: args.lang,
source_country: args.country,
domain: args.domain,
theme: args.theme,
tone_min: args.tone_min,
tone_max: args.tone_max,
};
let response = api.search(params).await?;
let output = OutputWriter::new(global);
if args.enrich || args.fetch_text {
if !global.quiet {
eprintln!("Enriching {} articles...", response.articles.len());
}
let service = if args.fetch_text {
EnrichmentService::with_fetcher(client)?
} else {
EnrichmentService::new()?
};
let inputs: Vec<ArticleInput> = response
.articles
.iter()
.map(|a| ArticleInput::with_metadata(
a.url.clone(),
Some(a.title.clone()),
Some(a.seendate.clone()),
Some(a.domain.clone()),
Some(a.language.clone()),
))
.collect();
let (enriched, stats) = service
.enrich_batch(inputs, args.fetch_text, args.fetch_concurrency)
.await;
let result = serde_json::json!({
"articles": enriched,
"enrichment_stats": stats,
});
output.write_value(&result)?;
if !global.quiet {
eprintln!();
eprintln!("Enrichment complete:");
eprintln!(" GKG found: {}/{}", stats.gkg_found, stats.total);
eprintln!(" GKG local: {}", stats.gkg_local);
if args.fetch_text {
eprintln!(" Text fetched: {}", stats.text_fetched);
}
}
} else {
output.write_value(&response)?;
}
Ok(ExitStatus::Success)
}
DocCommand::Timeline(args) => {
let mode = match args.mode {
TimelineMode::Vol => "vol",
TimelineMode::VolRaw => "volraw",
TimelineMode::Tone => "tone",
TimelineMode::Lang => "lang",
TimelineMode::SourceCountry => "sourcecountry",
};
let params = DocTimelineParams {
query: args.query,
mode: Some(mode.to_string()),
timespan: Some(args.timespan),
smooth: args.smooth,
};
let response = api.timeline(params).await?;
let output = OutputWriter::new(global);
output.write_value(&response)?;
Ok(ExitStatus::Success)
}
DocCommand::Wordcloud(args) => {
let params = DocWordcloudParams {
query: args.query,
timespan: Some(args.timespan),
};
let response = api.wordcloud(params).await?;
let output = OutputWriter::new(global);
output.write_value(&response)?;
Ok(ExitStatus::Success)
}
DocCommand::Themes(args) => {
let themes = get_common_themes(args.filter.as_deref());
let output = OutputWriter::new(global);
output.write_value(&serde_json::json!({ "themes": themes }))?;
Ok(ExitStatus::Success)
}
}
}
fn get_common_themes(filter: Option<&str>) -> Vec<&'static str> {
let all_themes = vec![
"AFFECT", "ARMEDCONFLICT", "ARREST", "BAN", "BANKRUPTCY",
"CEASEFIRE", "CONSPIRACY", "CORRUPTION", "COUP", "CRIME",
"CRISISLEX", "CYBER_ATTACK", "DEMOCRACY", "DIPLOMATIC_MEETING",
"DISASTER", "DISEASE", "ECONOMY", "EDUCATION", "ELECTION",
"EMBARGO", "EMIGRATION", "EMPLOYMENT", "ENVIRONMENT", "EPIDEMIC",
"EVACUATION", "FAMINE", "FINANCIAL_CRISIS", "FOOD_SECURITY",
"GENERAL_GOVERNMENT", "HEALTH", "HOSTAGE", "HUMANITARIAN",
"HUMAN_RIGHTS", "IMMIGRATION", "INFRASTRUCTURE", "INTERNET",
"KILL", "LABOR", "LEGISLATION", "MANMADE_DISASTER", "MEDIA",
"MILITARY", "NATURAL_DISASTER", "NEGOTIATION", "NUCLEAR",
"PEACEKEEPING", "POLITICAL_TURMOIL", "POLLUTION", "POVERTY",
"PROTEST", "PUBLIC_HEALTH", "REBELLION", "REFUGEE", "RELIGION",
"SANCTIONS", "SCIENCE", "SECURITY", "SOCIAL_MEDIA", "SOVEREIGNTY",
"SPACE", "SPORTS", "SURVEILLANCE", "TAX", "TECHNOLOGY",
"TERROR", "TORTURE", "TRADE", "UNEMPLOYMENT", "VIOLENCE",
"WAR_CRIME", "WATER", "WEATHER", "WMD",
];
match filter {
Some(f) => {
let f_upper = f.to_uppercase();
all_themes
.into_iter()
.filter(|t| t.contains(&f_upper))
.collect()
}
None => all_themes,
}
}