ngdp_client/
wago_api.rs

1//! Wago Tools API client for retrieving build history
2
3use chrono::{DateTime, Utc};
4use ngdp_cache::generic::GenericCache;
5use serde::{Deserialize, Serialize};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8/// Base URL for Wago Tools API
9const WAGO_API_BASE: &str = "https://wago.tools/api";
10
11/// Cache TTL for Wago builds API (30 minutes)
12const WAGO_CACHE_TTL_SECS: u64 = 30 * 60;
13
14/// Build information from Wago Tools API
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct WagoBuild {
17    /// Product identifier (e.g., "wow", "wowt", "wowxptr")
18    pub product: String,
19
20    /// Build version string (e.g., "11.0.5.57212")
21    pub version: String,
22
23    /// Timestamp when the build was created
24    pub created_at: String,
25
26    /// Build configuration hash
27    pub build_config: String,
28
29    /// Product configuration (optional)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub product_config: Option<String>,
32
33    /// CDN configuration hash (optional)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub cdn_config: Option<String>,
36
37    /// Whether this is a background download build
38    pub is_bgdl: bool,
39}
40
41/// Response from the builds API endpoint
42#[derive(Debug, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum WagoBuildsResponse {
45    /// Response is a map of product names to build arrays
46    Map(std::collections::HashMap<String, Vec<WagoBuild>>),
47    /// Response is a flat array of builds
48    Array(Vec<WagoBuild>),
49}
50
51/// Fetch build history from Wago Tools API (uncached)
52async fn fetch_builds_uncached() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
53    let client = reqwest::Client::new();
54    let url = format!("{WAGO_API_BASE}/builds");
55
56    let response = client
57        .get(&url)
58        .header("User-Agent", "ngdp-client")
59        .send()
60        .await?;
61
62    if !response.status().is_success() {
63        return Err(format!("Wago API returned status: {}", response.status()).into());
64    }
65
66    let builds = response.json::<WagoBuildsResponse>().await?;
67    Ok(builds)
68}
69
70/// Fetch build history from Wago Tools API with caching
71pub async fn fetch_builds() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
72    // Check if caching is disabled globally
73    let cache_enabled = crate::cached_client::is_caching_enabled();
74
75    if !cache_enabled {
76        return fetch_builds_uncached().await;
77    }
78
79    // Initialize cache
80    let cache = match GenericCache::with_subdirectory("wago").await {
81        Ok(cache) => cache,
82        Err(_) => {
83            // If cache initialization fails, fall back to uncached
84            return fetch_builds_uncached().await;
85        }
86    };
87
88    let cache_key = "builds.json";
89    let meta_key = "builds.meta";
90
91    // Check if cached data exists and is valid
92    let cache_path = cache.get_path(cache_key);
93    let meta_path = cache.get_path(meta_key);
94
95    // Check cache validity
96    if let Ok(metadata_content) = tokio::fs::read_to_string(&meta_path).await {
97        if let Ok(timestamp) = metadata_content.trim().parse::<u64>() {
98            let now = SystemTime::now()
99                .duration_since(UNIX_EPOCH)
100                .unwrap_or_default()
101                .as_secs();
102
103            if now < timestamp + WAGO_CACHE_TTL_SECS {
104                // Cache is still valid, try to read it
105                if let Ok(cached_data) = tokio::fs::read(&cache_path).await {
106                    if let Ok(builds) = serde_json::from_slice(&cached_data) {
107                        tracing::debug!("Using cached Wago builds data");
108                        return Ok(builds);
109                    }
110                }
111            }
112        }
113    }
114
115    // Cache miss or invalid, fetch fresh data
116    tracing::debug!("Fetching fresh Wago builds data");
117    let builds = fetch_builds_uncached().await?;
118
119    // Cache the response
120    if let Ok(json_data) = serde_json::to_vec(&builds) {
121        // Write cache data
122        let _ = cache.write(cache_key, &json_data).await;
123
124        // Write metadata (timestamp)
125        let timestamp = SystemTime::now()
126            .duration_since(UNIX_EPOCH)
127            .unwrap_or_default()
128            .as_secs();
129        let _ = cache
130            .write(meta_key, timestamp.to_string().as_bytes())
131            .await;
132    }
133
134    Ok(builds)
135}
136
137/// Filter builds by product name
138pub fn filter_builds_by_product(builds: WagoBuildsResponse, product: &str) -> Vec<WagoBuild> {
139    match builds {
140        WagoBuildsResponse::Map(map) => map.get(product).cloned().unwrap_or_default(),
141        WagoBuildsResponse::Array(builds) => builds
142            .into_iter()
143            .filter(|b| b.product == product)
144            .collect(),
145    }
146}
147
148/// Extract build ID from version string (e.g., "1.15.2.55140" -> "55140")
149pub fn extract_build_id(version: &str) -> Option<String> {
150    version.split('.').next_back().map(|s| s.to_string())
151}
152
153/// Find a specific build by build ID in the filtered builds
154pub fn find_build_by_id<'a>(builds: &'a [WagoBuild], build_id: &str) -> Option<&'a WagoBuild> {
155    builds.iter().find(|build| {
156        // Try exact match on build ID extracted from version
157        if let Some(extracted_id) = extract_build_id(&build.version) {
158            extracted_id == build_id
159        } else {
160            // Also try direct version string match
161            build.version == build_id
162        }
163    })
164}
165
166/// Parse a date string from Wago API format to DateTime
167pub fn parse_wago_date(date_str: &str) -> Option<DateTime<Utc>> {
168    // Wago uses format: "2025-07-14 22:25:16"
169    DateTime::parse_from_str(&format!("{date_str} +00:00"), "%Y-%m-%d %H:%M:%S %z")
170        .ok()
171        .map(|dt| dt.with_timezone(&Utc))
172}