1use std::fmt;
2use std::fs::File;
3use std::io::Write;
4
5use base64::{engine::general_purpose, Engine as _};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::McError;
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct ServerStatus {
14 pub online: bool,
15 pub ip: String,
16 pub port: u16,
17 pub hostname: String,
18 pub latency: f64,
19 pub dns: Option<DnsInfo>,
20 pub data: ServerData,
21}
22
23#[derive(Debug, Serialize, Deserialize, Clone)]
24#[serde(untagged)]
25pub enum ServerData {
26 Java(JavaStatus),
27 Bedrock(BedrockStatus),
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone)]
32pub struct DnsInfo {
33 pub a_records: Vec<String>,
34 pub cname: Option<String>,
35 pub ttl: u32,
36}
37
38#[derive(Serialize, Deserialize, Clone)]
40pub struct JavaStatus {
41 pub version: JavaVersion,
42 pub players: JavaPlayers,
43 pub description: String,
44 #[serde(skip_serializing)]
45 pub favicon: Option<String>,
46 pub map: Option<String>,
47 pub gamemode: Option<String>,
48 pub software: Option<String>,
49 pub plugins: Option<Vec<JavaPlugin>>,
50 pub mods: Option<Vec<JavaMod>>,
51 #[serde(skip)]
52 pub raw_data: Value,
53}
54
55#[derive(Debug, Serialize, Deserialize, Clone)]
56pub struct JavaVersion {
57 pub name: String,
58 pub protocol: i64,
59}
60
61#[derive(Debug, Serialize, Deserialize, Clone)]
62pub struct JavaPlayers {
63 pub online: i64,
64 pub max: i64,
65 pub sample: Option<Vec<JavaPlayer>>,
66}
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct JavaPlayer {
70 pub name: String,
71 pub id: String,
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct JavaPlugin {
76 pub name: String,
77 pub version: Option<String>,
78}
79
80#[derive(Debug, Serialize, Deserialize, Clone)]
81pub struct JavaMod {
82 pub modid: String,
83 pub version: Option<String>,
84}
85
86#[derive(Serialize, Deserialize, Clone)]
88pub struct BedrockStatus {
89 pub edition: String,
90 pub motd: String,
91 pub protocol_version: String,
92 pub version: String,
93 pub online_players: String,
94 pub max_players: String,
95 pub server_uid: String,
96 pub motd2: String,
97 pub game_mode: String,
98 pub game_mode_numeric: String,
99 pub port_ipv4: String,
100 pub port_ipv6: String,
101 pub map: Option<String>,
102 pub software: Option<String>,
103 pub raw_data: String,
104}
105
106#[derive(Debug, Serialize, Deserialize, Clone)]
108pub struct ServerInfo {
109 pub address: String,
110 pub edition: ServerEdition,
111}
112
113#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
114pub enum ServerEdition {
115 Java,
116 Bedrock,
117}
118
119impl fmt::Debug for JavaStatus {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.debug_struct("JavaStatus")
123 .field("version", &self.version)
124 .field("players", &self.players)
125 .field("description", &self.description)
126 .field("map", &self.map)
127 .field("gamemode", &self.gamemode)
128 .field("software", &self.software)
129 .field("plugins", &self.plugins.as_ref().map(|p| p.len()))
130 .field("mods", &self.mods.as_ref().map(|m| m.len()))
131 .field("favicon", &self.favicon.as_ref().map(|_| "[Favicon data]"))
132 .field("raw_data", &"[Value]")
133 .finish()
134 }
135}
136
137impl JavaStatus {
138 pub fn save_favicon(&self, filename: &str) -> Result<(), McError> {
139 if let Some(favicon) = &self.favicon {
140 let data = favicon.split(',').nth(1).unwrap_or(favicon);
141 let bytes = general_purpose::STANDARD
142 .decode(data)
143 .map_err(McError::Base64Error)?;
144
145 let mut file = File::create(filename).map_err(McError::IoError)?;
146 file.write_all(&bytes).map_err(McError::IoError)?;
147
148 Ok(())
149 } else {
150 Err(McError::InvalidResponse("No favicon available".to_string()))
151 }
152 }
153}
154
155impl fmt::Debug for BedrockStatus {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 f.debug_struct("BedrockStatus")
158 .field("edition", &self.edition)
159 .field("motd", &self.motd)
160 .field("protocol_version", &self.protocol_version)
161 .field("version", &self.version)
162 .field("online_players", &self.online_players)
163 .field("max_players", &self.max_players)
164 .field("server_uid", &self.server_uid)
165 .field("motd2", &self.motd2)
166 .field("game_mode", &self.game_mode)
167 .field("game_mode_numeric", &self.game_mode_numeric)
168 .field("port_ipv4", &self.port_ipv4)
169 .field("port_ipv6", &self.port_ipv6)
170 .field("map", &self.map)
171 .field("software", &self.software)
172 .field("raw_data", &self.raw_data.len())
173 .finish()
174 }
175}
176
177impl std::str::FromStr for ServerEdition {
178 type Err = McError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 match s.to_lowercase().as_str() {
182 "java" => Ok(ServerEdition::Java),
183 "bedrock" => Ok(ServerEdition::Bedrock),
184 _ => Err(McError::InvalidEdition(s.to_string())),
185 }
186 }
187}