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}