rust_mc_status/
models.rs

1//! Data models for Minecraft server status responses.
2//!
3//! This module provides structured data types for representing server status
4//! information from both Java Edition and Bedrock Edition servers.
5//!
6//! # Examples
7//!
8//! ## Basic Usage
9//!
10//! ```no_run
11//! use rust_mc_status::{McClient, ServerEdition};
12//!
13//! # #[tokio::main]
14//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
15//! let client = McClient::new();
16//! let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
17//!
18//! println!("Server: {}:{}", status.ip, status.port);
19//! println!("Online: {}", status.online);
20//! println!("Latency: {:.2}ms", status.latency);
21//!
22//! match status.data {
23//!     rust_mc_status::ServerData::Java(java) => {
24//!         println!("Players: {}/{}", java.players.online, java.players.max);
25//!     }
26//!     rust_mc_status::ServerData::Bedrock(bedrock) => {
27//!         println!("Players: {}/{}", bedrock.online_players, bedrock.max_players);
28//!     }
29//! }
30//! # Ok(())
31//! # }
32//! ```
33
34use std::fmt;
35use std::fs::File;
36use std::io::Write;
37
38use base64::{engine::general_purpose, Engine as _};
39use serde::{Deserialize, Serialize};
40use serde_json::Value;
41
42use crate::McError;
43
44/// Server status information.
45///
46/// This structure contains all information about a Minecraft server's status.
47/// Even if the server is offline, some fields may still be populated (e.g., DNS info).
48///
49/// # Example
50///
51/// ```no_run
52/// use rust_mc_status::{McClient, ServerEdition};
53///
54/// # #[tokio::main]
55/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
56/// let client = McClient::new();
57/// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
58///
59/// println!("Hostname: {}", status.hostname);
60/// println!("IP: {}", status.ip);
61/// println!("Port: {}", status.port);
62/// println!("Online: {}", status.online);
63/// println!("Latency: {:.2}ms", status.latency);
64/// # Ok(())
65/// # }
66/// ```
67#[derive(Debug, Serialize, Deserialize, Clone)]
68pub struct ServerStatus {
69    /// Whether the server is online and responding.
70    ///
71    /// This field is always `true` for successful pings. If the server
72    /// is offline or unreachable, the `ping()` method returns an error instead.
73    pub online: bool,
74
75    /// Resolved IP address of the server.
76    ///
77    /// This is the actual IP address that was connected to, which may differ
78    /// from the hostname if SRV records were used or if the hostname resolves
79    /// to multiple IP addresses.
80    ///
81    /// Example: `"172.65.197.160"`
82    pub ip: String,
83
84    /// Port number of the server.
85    ///
86    /// This is the actual port that was connected to. For Java servers, this
87    /// may differ from the default port (25565) if an SRV record was found.
88    ///
89    /// Example: `25565` (Java) or `19132` (Bedrock)
90    pub port: u16,
91
92    /// Original hostname used for the query.
93    ///
94    /// This is the hostname that was provided to the `ping()` method, before
95    /// any DNS resolution or SRV lookup.
96    ///
97    /// Example: `"mc.hypixel.net"`
98    pub hostname: String,
99
100    /// Latency in milliseconds.
101    ///
102    /// This is the round-trip time (RTT) from sending the ping request to
103    /// receiving the response. Lower values indicate better network connectivity.
104    ///
105    /// Example: `45.23` (45.23 milliseconds)
106    pub latency: f64,
107
108    /// Optional DNS information (A records, CNAME, TTL).
109    ///
110    /// This field contains DNS resolution details if available. It may be `None`
111    /// if DNS information could not be retrieved or if an IP address was used
112    /// directly instead of a hostname.
113    pub dns: Option<DnsInfo>,
114
115    /// Server data (Java or Bedrock specific information).
116    ///
117    /// This field contains edition-specific server information including version,
118    /// players, plugins, mods, and more. Use pattern matching to access the data:
119    ///
120    /// ```no_run
121    /// # use rust_mc_status::ServerData;
122    /// # let data = ServerData::Java(rust_mc_status::JavaStatus {
123    /// #     version: rust_mc_status::JavaVersion { name: "1.20.1".to_string(), protocol: 763 },
124    /// #     players: rust_mc_status::JavaPlayers { online: 0, max: 100, sample: None },
125    /// #     description: "".to_string(),
126    /// #     favicon: None,
127    /// #     map: None,
128    /// #     gamemode: None,
129    /// #     software: None,
130    /// #     plugins: None,
131    /// #     mods: None,
132    /// #     raw_data: serde_json::Value::Null,
133    /// # });
134    /// match data {
135    ///     ServerData::Java(java) => println!("Java server: {}", java.version.name),
136    ///     ServerData::Bedrock(bedrock) => println!("Bedrock server: {}", bedrock.version),
137    /// }
138    /// ```
139    pub data: ServerData,
140}
141
142impl ServerStatus {
143    /// Get player count information.
144    ///
145    /// Returns a tuple of `(online, max)` players, or `None` if not available.
146    ///
147    /// # Example
148    ///
149    /// ```no_run
150    /// # use rust_mc_status::{McClient, ServerEdition};
151    /// # #[tokio::main]
152    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
153    /// # let client = McClient::new();
154    /// # let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
155    /// if let Some((online, max)) = status.players() {
156    ///     println!("Players: {}/{}", online, max);
157    /// }
158    /// # Ok(())
159    /// # }
160    /// ```
161    pub fn players(&self) -> Option<(i64, i64)> {
162        match &self.data {
163            ServerData::Java(java) => Some((java.players.online, java.players.max)),
164            ServerData::Bedrock(bedrock) => {
165                let online = bedrock.online_players.parse().ok()?;
166                let max = bedrock.max_players.parse().ok()?;
167                Some((online, max))
168            }
169        }
170    }
171}
172
173/// Server data (Java or Bedrock specific).
174///
175/// This enum contains edition-specific server information.
176#[derive(Debug, Serialize, Deserialize, Clone)]
177#[serde(untagged)]
178pub enum ServerData {
179    /// Java Edition server data.
180    Java(JavaStatus),
181    /// Bedrock Edition server data.
182    Bedrock(BedrockStatus),
183}
184
185impl ServerData {
186    /// Get player count if available.
187    ///
188    /// Returns `(online, max)` for Java servers, or parsed values for Bedrock servers.
189    pub fn players(&self) -> Option<(i64, i64)> {
190        match self {
191            ServerData::Java(java) => Some((java.players.online, java.players.max)),
192            ServerData::Bedrock(bedrock) => {
193                let online = bedrock.online_players.parse().ok()?;
194                let max = bedrock.max_players.parse().ok()?;
195                Some((online, max))
196            }
197        }
198    }
199}
200
201/// DNS information about the server.
202///
203/// Contains resolved A records, optional CNAME, and TTL information.
204/// This information is retrieved during DNS resolution and cached for 5 minutes.
205///
206/// # Example
207///
208/// ```no_run
209/// use rust_mc_status::{McClient, ServerEdition};
210///
211/// # #[tokio::main]
212/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
213/// let client = McClient::new();
214/// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
215///
216/// if let Some(dns) = status.dns {
217///     println!("A records: {:?}", dns.a_records);
218///     if let Some(cname) = dns.cname {
219///         println!("CNAME: {}", cname);
220///     }
221///     println!("TTL: {} seconds", dns.ttl);
222/// }
223/// # Ok(())
224/// # }
225/// ```
226#[derive(Debug, Serialize, Deserialize, Clone)]
227pub struct DnsInfo {
228    /// A record IP addresses.
229    ///
230    /// This is a list of IPv4 and IPv6 addresses that the hostname resolves to.
231    /// Typically contains one or more IP addresses.
232    ///
233    /// Example: `vec!["172.65.197.160".to_string()]`
234    pub a_records: Vec<String>,
235
236    /// Optional CNAME record.
237    ///
238    /// If the hostname is a CNAME (canonical name), this field contains the
239    /// canonical hostname. Most servers don't use CNAME records.
240    ///
241    /// Example: `Some("canonical.example.com".to_string())`
242    pub cname: Option<String>,
243
244    /// Time-to-live in seconds.
245    ///
246    /// This is the DNS cache TTL used by the library. DNS records are cached
247    /// for this duration to improve performance.
248    ///
249    /// Default: `300` (5 minutes)
250    pub ttl: u32,
251}
252
253/// Java Edition server status.
254///
255/// Contains detailed information about a Java Edition server, including version,
256/// players, plugins, mods, and more.
257///
258/// # Example
259///
260/// ```no_run
261/// use rust_mc_status::{McClient, ServerEdition};
262///
263/// # #[tokio::main]
264/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
265/// let client = McClient::new();
266/// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
267///
268/// if let rust_mc_status::ServerData::Java(java) = status.data {
269///     println!("Version: {}", java.version.name);
270///     println!("Players: {}/{}", java.players.online, java.players.max);
271///     println!("Description: {}", java.description);
272///     
273///     if let Some(plugins) = &java.plugins {
274///         println!("Plugins: {}", plugins.len());
275///     }
276/// }
277/// # Ok(())
278/// # }
279/// ```
280#[derive(Serialize, Deserialize, Clone)]
281pub struct JavaStatus {
282    /// Server version information.
283    pub version: JavaVersion,
284    /// Player information.
285    pub players: JavaPlayers,
286    /// Server description (MOTD).
287    pub description: String,
288    /// Base64-encoded favicon (PNG image data).
289    #[serde(skip_serializing)]
290    pub favicon: Option<String>,
291    /// Current map name.
292    pub map: Option<String>,
293    /// Game mode.
294    pub gamemode: Option<String>,
295    /// Server software (e.g., "Paper", "Spigot", "Vanilla").
296    pub software: Option<String>,
297    /// List of installed plugins.
298    pub plugins: Option<Vec<JavaPlugin>>,
299    /// List of installed mods.
300    pub mods: Option<Vec<JavaMod>>,
301    /// Raw JSON data from server response.
302    #[serde(skip)]
303    pub raw_data: Value,
304}
305
306impl JavaStatus {
307    /// Save the server favicon to a file.
308    ///
309    /// The favicon is decoded from base64 and saved as a PNG image.
310    ///
311    /// # Arguments
312    ///
313    /// * `filename` - Path where the favicon should be saved
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if:
318    /// - No favicon is available
319    /// - Base64 decoding fails
320    /// - File I/O fails
321    ///
322    /// # Example
323    ///
324    /// ```no_run
325    /// use rust_mc_status::{McClient, ServerEdition};
326    ///
327    /// # #[tokio::main]
328    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
329    /// let client = McClient::new();
330    /// let status = client.ping("mc.hypixel.net", ServerEdition::Java).await?;
331    ///
332    /// if let rust_mc_status::ServerData::Java(java) = status.data {
333    ///     java.save_favicon("server_icon.png")?;
334    /// }
335    /// # Ok(())
336    /// # }
337    /// ```
338    pub fn save_favicon(&self, filename: &str) -> Result<(), McError> {
339        if let Some(favicon) = &self.favicon {
340            let data = favicon.split(',').nth(1).unwrap_or(favicon);
341            let bytes = general_purpose::STANDARD
342                .decode(data)
343                .map_err(McError::Base64Error)?;
344
345            let mut file = File::create(filename).map_err(McError::IoError)?;
346            file.write_all(&bytes).map_err(McError::IoError)?;
347
348            Ok(())
349        } else {
350            Err(McError::InvalidResponse("No favicon available".to_string()))
351        }
352    }
353}
354
355impl fmt::Debug for JavaStatus {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        f.debug_struct("JavaStatus")
358            .field("version", &self.version)
359            .field("players", &self.players)
360            .field("description", &self.description)
361            .field("map", &self.map)
362            .field("gamemode", &self.gamemode)
363            .field("software", &self.software)
364            .field("plugins", &self.plugins.as_ref().map(|p| p.len()))
365            .field("mods", &self.mods.as_ref().map(|m| m.len()))
366            .field("favicon", &self.favicon.as_ref().map(|_| "[Favicon data]"))
367            .field("raw_data", &"[Value]")
368            .finish()
369    }
370}
371
372/// Java Edition server version information.
373#[derive(Debug, Serialize, Deserialize, Clone)]
374pub struct JavaVersion {
375    /// Version name (e.g., "1.20.1").
376    pub name: String,
377    /// Protocol version number.
378    pub protocol: i64,
379}
380
381/// Java Edition player information.
382#[derive(Debug, Serialize, Deserialize, Clone)]
383pub struct JavaPlayers {
384    /// Number of players currently online.
385    pub online: i64,
386    /// Maximum number of players.
387    pub max: i64,
388    /// Sample of online players (if provided by server).
389    pub sample: Option<Vec<JavaPlayer>>,
390}
391
392/// Java Edition player sample.
393#[derive(Debug, Serialize, Deserialize, Clone)]
394pub struct JavaPlayer {
395    /// Player name.
396    pub name: String,
397    /// Player UUID.
398    pub id: String,
399}
400
401/// Java Edition plugin information.
402#[derive(Debug, Serialize, Deserialize, Clone)]
403pub struct JavaPlugin {
404    /// Plugin name.
405    pub name: String,
406    /// Plugin version (if available).
407    pub version: Option<String>,
408}
409
410/// Java Edition mod information.
411#[derive(Debug, Serialize, Deserialize, Clone)]
412pub struct JavaMod {
413    /// Mod ID.
414    pub modid: String,
415    /// Mod version (if available).
416    pub version: Option<String>,
417}
418
419/// Bedrock Edition server status.
420///
421/// Contains information about a Bedrock Edition server.
422///
423/// # Example
424///
425/// ```no_run
426/// use rust_mc_status::{McClient, ServerEdition};
427///
428/// # #[tokio::main]
429/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
430/// let client = McClient::new();
431/// let status = client.ping("geo.hivebedrock.network:19132", ServerEdition::Bedrock).await?;
432///
433/// if let rust_mc_status::ServerData::Bedrock(bedrock) = status.data {
434///     println!("Edition: {}", bedrock.edition);
435///     println!("Version: {}", bedrock.version);
436///     println!("Players: {}/{}", bedrock.online_players, bedrock.max_players);
437///     println!("MOTD: {}", bedrock.motd);
438/// }
439/// # Ok(())
440/// # }
441/// ```
442#[derive(Serialize, Deserialize, Clone)]
443pub struct BedrockStatus {
444    /// Minecraft edition (e.g., "MCPE").
445    pub edition: String,
446    /// Message of the day.
447    pub motd: String,
448    /// Protocol version.
449    pub protocol_version: String,
450    /// Server version.
451    pub version: String,
452    /// Number of online players (as string).
453    pub online_players: String,
454    /// Maximum number of players (as string).
455    pub max_players: String,
456    /// Server UID.
457    pub server_uid: String,
458    /// Secondary MOTD.
459    pub motd2: String,
460    /// Game mode.
461    pub game_mode: String,
462    /// Game mode numeric value.
463    pub game_mode_numeric: String,
464    /// IPv4 port.
465    pub port_ipv4: String,
466    /// IPv6 port.
467    pub port_ipv6: String,
468    /// Current map name.
469    pub map: Option<String>,
470    /// Server software.
471    pub software: Option<String>,
472    /// Raw response data.
473    pub raw_data: String,
474}
475
476impl fmt::Debug for BedrockStatus {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        f.debug_struct("BedrockStatus")
479            .field("edition", &self.edition)
480            .field("motd", &self.motd)
481            .field("protocol_version", &self.protocol_version)
482            .field("version", &self.version)
483            .field("online_players", &self.online_players)
484            .field("max_players", &self.max_players)
485            .field("server_uid", &self.server_uid)
486            .field("motd2", &self.motd2)
487            .field("game_mode", &self.game_mode)
488            .field("game_mode_numeric", &self.game_mode_numeric)
489            .field("port_ipv4", &self.port_ipv4)
490            .field("port_ipv6", &self.port_ipv6)
491            .field("map", &self.map)
492            .field("software", &self.software)
493            .field("raw_data", &self.raw_data.len())
494            .finish()
495    }
496}
497
498/// Server information for batch queries.
499///
500/// Used to specify multiple servers to ping in parallel.
501///
502/// # Example
503///
504/// ```no_run
505/// use rust_mc_status::{McClient, ServerEdition, ServerInfo};
506///
507/// # #[tokio::main]
508/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
509/// let client = McClient::new();
510/// let servers = vec![
511///     ServerInfo {
512///         address: "mc.hypixel.net".to_string(),
513///         edition: ServerEdition::Java,
514///     },
515///     ServerInfo {
516///         address: "geo.hivebedrock.network:19132".to_string(),
517///         edition: ServerEdition::Bedrock,
518///     },
519/// ];
520///
521/// let results = client.ping_many(&servers).await;
522/// for (server, result) in results {
523///     println!("{}: {:?}", server.address, result.is_ok());
524/// }
525/// # Ok(())
526/// # }
527/// ```
528#[derive(Debug, Serialize, Deserialize, Clone)]
529pub struct ServerInfo {
530    /// Server address (hostname or IP, optionally with port).
531    pub address: String,
532    /// Server edition.
533    pub edition: ServerEdition,
534}
535
536/// Minecraft server edition.
537///
538/// Specifies whether the server is Java Edition or Bedrock Edition.
539#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
540pub enum ServerEdition {
541    /// Java Edition server (default port: 25565).
542    Java,
543    /// Bedrock Edition server (default port: 19132).
544    Bedrock,
545}
546
547/// Cache statistics.
548///
549/// Provides information about the current state of DNS and SRV caches.
550///
551/// # Example
552///
553/// ```no_run
554/// use rust_mc_status::McClient;
555///
556/// # #[tokio::main]
557/// # async fn main() {
558/// let client = McClient::new();
559/// let stats = client.cache_stats();
560/// println!("DNS entries: {}, SRV entries: {}", stats.dns_entries, stats.srv_entries);
561/// # }
562/// ```
563#[derive(Debug, Clone, Copy)]
564pub struct CacheStats {
565    /// Number of entries in DNS cache.
566    pub dns_entries: usize,
567    /// Number of entries in SRV cache.
568    pub srv_entries: usize,
569}
570
571impl std::str::FromStr for ServerEdition {
572    type Err = McError;
573
574    fn from_str(s: &str) -> Result<Self, Self::Err> {
575        match s.to_lowercase().as_str() {
576            "java" => Ok(ServerEdition::Java),
577            "bedrock" => Ok(ServerEdition::Bedrock),
578            _ => Err(McError::InvalidEdition(s.to_string())),
579        }
580    }
581}