stargazer/tier1/
xlx_api.rs

1//! XLX API XML reflector list fetcher.
2//!
3//! Fetches the live XLX reflector registry from
4//! `http://xlxapi.rlx.lu/api.php?do=GetReflectorList` and upserts each
5//! reflector into the `reflectors` Postgres table.
6//!
7//! The XLX API is the authoritative source for XLX-family reflectors. Unlike
8//! the Pi-Star host file (which only provides IP addresses), this feed includes
9//! dashboard URLs, uptime, country, and last-contact timestamps — making it the
10//! richest Tier 1 data source.
11//!
12//! All XLX reflectors expose a UDP JSON monitor on port 10001, so this fetcher
13//! sets `tier2_available = true` for every reflector it discovers, enabling
14//! automatic Tier 2 promotion.
15//!
16//! **Poll interval:** every 10 minutes (default 600 s). The XLX network is
17//! dynamic — reflectors come and go, and the API rate limit is generous enough
18//! for this cadence.
19
20use serde::Deserialize;
21
22use super::error::FetchError;
23use crate::db;
24
25/// XLX API endpoint URL.
26const XLX_API_URL: &str = "http://xlxapi.rlx.lu/api.php?do=GetReflectorList";
27
28/// Top-level XML envelope: `<XLXAPI>`.
29///
30/// The XLX API wraps everything in an `<answer>` element inside the root.
31#[derive(Debug, Deserialize)]
32#[serde(rename = "XLXAPI")]
33struct XlxApiResponse {
34    /// The `<answer>` wrapper element.
35    answer: XlxAnswer,
36}
37
38/// The `<answer>` element containing the reflector list.
39#[derive(Debug, Deserialize)]
40struct XlxAnswer {
41    /// The `<reflectorlist>` element containing individual `<reflector>` entries.
42    reflectorlist: XlxReflectorList,
43}
44
45/// The `<reflectorlist>` element — a wrapper around the reflector array.
46///
47/// Each child `<reflector>` element maps to one [`XlxReflector`] entry.
48#[derive(Debug, Deserialize)]
49struct XlxReflectorList {
50    /// Individual reflector entries. Uses `#[serde(default)]` so an empty
51    /// list deserializes to an empty `Vec` rather than failing.
52    #[serde(default)]
53    reflector: Vec<XlxReflector>,
54}
55
56/// A single `<reflector>` entry from the XLX API.
57///
58/// Only the fields needed for the reflector registry are deserialized; the
59/// rest (uptime, lastcontact, comment) are ignored via `#[serde(default)]` on
60/// the parent container and field-level defaults here.
61#[derive(Debug, Deserialize)]
62struct XlxReflector {
63    /// Reflector callsign, e.g. `"XLX000"`, `"XLX320"`.
64    name: String,
65
66    /// Reflector IP address (the `<lastip>` element).
67    #[serde(default)]
68    lastip: String,
69
70    /// URL of the reflector's web dashboard, if available.
71    #[serde(default)]
72    dashboardurl: Option<String>,
73
74    /// Country or region string (e.g. `"USA - Florida"`).
75    #[serde(default)]
76    country: Option<String>,
77}
78
79/// Fetches the XLX reflector list and upserts all entries into Postgres.
80///
81/// Returns the number of reflectors successfully upserted. After upserting
82/// each reflector, also sets `tier2_available = true` since all XLX reflectors
83/// support the UDP JSON monitor protocol on port 10001.
84///
85/// # Errors
86///
87/// - [`FetchError::Http`] if the HTTP request fails.
88/// - [`FetchError::Xml`] if the XML response cannot be deserialized.
89/// - [`FetchError::Database`] if any database operation fails.
90pub(crate) async fn fetch_and_store(
91    client: &reqwest::Client,
92    pool: &sqlx::PgPool,
93) -> Result<usize, FetchError> {
94    // Fetch raw XML text, then deserialize with quick-xml's serde support.
95    let body = client.get(XLX_API_URL).send().await?.text().await?;
96    let response: XlxApiResponse = quick_xml::de::from_str(&body)?;
97
98    let mut count = 0usize;
99    for entry in &response.answer.reflectorlist.reflector {
100        // Determine IP: use the lastip field if non-empty.
101        let ip = if entry.lastip.is_empty() {
102            None
103        } else {
104            Some(entry.lastip.as_str())
105        };
106
107        // XLX reflectors use the DExtra protocol as their primary link protocol.
108        db::reflectors::upsert(
109            pool,
110            &entry.name,
111            "dextra",
112            ip,
113            entry.dashboardurl.as_deref(),
114            entry.country.as_deref(),
115        )
116        .await?;
117
118        // All XLX reflectors expose the UDP JSON monitor on port 10001.
119        db::reflectors::set_tier2_available(pool, &entry.name, true).await?;
120
121        count += 1;
122    }
123
124    tracing::info!(count, "xlx-api: upserted reflectors");
125    Ok(count)
126}