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