1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! Implementation of the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol

mod packet;

use self::packet::{Packet, PacketId};
use crate::{
    errors::MinecraftProtocolError,
    socket::{ReadWriteMinecraftString, ReadWriteVarInt},
    varint::VarInt,
};
use serde::{Deserialize, Serialize};
use tokio::{
    io::{self, AsyncWriteExt, Interest},
    net::TcpStream,
};

/// Response from the server with status information.
/// Represents [this JSON object](https://wiki.vg/Server_List_Ping#Status_Response)
/// to be serialized and deserialized.
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusResponse {
    /// Information about the game and protocol version.
    /// See [Version] for more information.
    pub version: Version,

    // Information about players on the server.
    /// See [Players] for more information.
    pub players: Players,

    /// The "motd" - message shown in the server list by the client.
    #[serde(rename = "description")]
    pub motd: ChatObject,

    /// URI to the server's favicon.
    pub favicon: Option<String>,

    /// Does the server preview chat?
    #[serde(rename = "previewsChat")]
    pub previews_chat: Option<bool>,

    /// Does the server use signed chat messages?
    /// Only returned for servers post 1.19.1
    #[serde(rename = "enforcesSecureChat")]
    pub enforces_secure_chat: Option<bool>,
}

/// Struct that stores information about players on the server.
///
/// Not intended to be used directly, but only as a part of [StatusResponse].
#[derive(Debug, Serialize, Deserialize)]
pub struct Players {
    /// The maximum number of players allowed on the server.
    pub max: u32,

    /// The number of players currently online.
    pub online: u32,

    /// A listing of some online Players.
    /// See [Sample] for more information.
    pub sample: Option<Vec<Sample>>,
}

/// A player listed on the server's list ping information.
///
/// Not intended to be used directly, but only as a part of [StatusResponse].
#[derive(Debug, Serialize, Deserialize)]
pub struct Sample {
    /// The player's username.
    pub name: String,

    /// The player's UUID.
    pub id: String,
}

/// Struct that stores version information about the server.
///
/// Not intended to be used directly, but only as a part of [StatusResponse].
#[derive(Debug, Serialize, Deserialize)]
pub struct Version {
    /// The game version (e.g: 1.19.1)
    pub name: String,
    /// The version of the [Protocol](https://wiki.vg/Protocol) being used.
    ///
    /// See [the wiki.vg page](https://wiki.vg/Protocol_version_numbers) for a
    /// reference on what versions these correspond to.
    pub protocol: u16,
}

/// Represents a chat object (the MOTD is sent as a chat object).
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatObject {
    /// An individual chat object
    Object(ChatComponentObject),

    /// Vector of multiple chat objects
    Array(Vec<ChatObject>),

    /// Unknown data - raw JSON
    JsonPrimitive(serde_json::Value),
}

/// A piece of a `ChatObject`
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatComponentObject {
    /// Text of the chat message
    pub text: Option<String>,

    /// Translation key if the message needs to pull from the language file.
    /// See [wiki.vg](https://wiki.vg/Chat#Translation_component)
    pub translate: Option<String>,

    /// Displays the keybind for the specified key, or the string itself if unknown.
    pub keybind: Option<String>,

    /// Should the text be rendered **bold**?
    pub bold: Option<bool>,

    /// Should the text be rendered *italic*?
    pub italic: Option<bool>,

    /// Should the text be rendered __underlined__?
    pub underlined: Option<bool>,

    /// Should the text be rendered as ~~strikethrough~~
    pub strikethrough: Option<bool>,

    /// Should the text be rendered as obfuscated?
    /// Switching randomly between characters of the same width
    pub obfuscated: Option<bool>,

    /// The font to use to render, comes in three options:
    /// * `minecraft:uniform` - Unicode font
    /// * `minecraft:alt` - enchanting table font
    /// * `minecraft:default` - font based on resource pack (1.16+)
    /// Any other value can be ignored
    pub font: Option<String>,

    /// The color to display the chat item in.
    /// Can be a [chat color](https://wiki.vg/Chat#Colors),
    /// [format code](https://wiki.vg/Chat#Styles),
    /// or any valid web color
    pub color: Option<String>,

    /// Text to insert into the chat box when shift-clicking this component
    pub insertion: Option<String>,

    /// Defines an event that occurs when this chat item is clicked
    #[serde(rename = "clickEvent")]
    pub click_event: Option<ChatClickEvent>,

    /// Defines an event that occurs when this chat item is hovered on
    #[serde(rename = "hoverEvent")]
    pub hover_event: Option<ChatHoverEvent>,

    /// Sibling components to this chat item.
    /// If present, will not be empty
    pub extra: Option<Vec<ChatObject>>,
}

/// ClickEvent data for a chat component
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatClickEvent {
    // These are not renamed on purpose. (server returns them in snake_case)
    /// Opens the URL in the user's default browser. Protocol must be `http` or `https`
    pub open_url: Option<String>,

    /// Runs the command.
    /// Simply causes the user to say the string in chat -
    /// so only has command effect if it starts with /
    ///
    /// Irrelevant for motd purposes.
    pub run_command: Option<String>,

    /// Replaces the content of the user's chat box with the given text.
    ///
    /// Irrelevant for motd purposes.
    pub suggest_command: Option<String>,

    /// Copies the given text into the client's clipboard.
    pub copy_to_clipboard: Option<String>,
}

/// HoverEvent data for a chat component
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatHoverEvent {
    // These are not renamed on purpose. (server returns them in snake_case)
    /// Text to show when the item is hovered over
    pub show_text: Option<Box<ChatObject>>,

    /// Same as show_text, but for servers < 1.16
    pub value: Option<Box<ChatObject>>,

    /// Displays the item of the given NBT
    pub show_item: Option<String>,

    /// Displays information about the entity with the given NBT
    pub show_entity: Option<String>,
}

/// Ping the server for information following the [Server List Ping](https://wiki.vg/Server_List_Ping) protocol.
///
/// # Arguments
/// * `host` - A string slice that holds the hostname of the server to connect to.
/// * `port` - The port to connect to on that server.
///
/// # Examples
/// ```
/// use mc_query::status;
/// use tokio::io::Result;
///
/// #[tokio::main]
/// async fn main() -> Result<()> {
///     let data = status("mc.hypixel.net", 25565).await?;
///     println!("{data:#?}");
///
///     Ok(())
/// }
/// ```
pub async fn status(host: &str, port: u16) -> io::Result<StatusResponse> {
    let mut socket = TcpStream::connect(format!("{host}:{port}")).await?;

    socket
        .ready(Interest::READABLE | Interest::WRITABLE)
        .await?;

    // handshake packet
    // https://wiki.vg/Server_List_Ping#Handshake
    let handshake = Packet::builder(PacketId::Handshake)
        .add_varint(&VarInt::from(-1))
        .add_string(host)
        .add_u16(port)
        .add_varint(&VarInt::from(PacketId::Status))
        .build();

    socket.write_all(&handshake.bytes()).await?;

    // status request packet
    // https://wiki.vg/Server_List_Ping#Status_Request
    let status_request = Packet::builder(PacketId::Handshake).build();
    socket.write_all(&status_request.bytes()).await?;

    // listen to status response
    // https://wiki.vg/Server_List_Ping#Status_Response
    let _len = socket.read_varint().await?;
    let id = socket.read_varint().await?;

    if id != 0 {
        return Err(MinecraftProtocolError::InvalidStatusResponse.into());
    }

    let data = socket.read_mc_string().await?;
    socket.shutdown().await?;

    serde_json::from_str::<StatusResponse>(&data)
        .map_err(|_| MinecraftProtocolError::InvalidStatusResponse.into())
}

#[cfg(test)]
mod tests {
    use super::status;
    use tokio::io::Result;

    #[tokio::test]
    async fn test_hypixel_status() -> Result<()> {
        let data = status("mc.hypixel.net", 25565).await?;
        println!("{data:#?}");

        Ok(())
    }

    #[tokio::test]
    async fn test_local_status() -> Result<()> {
        let data = status("localhost", 25565).await?;
        println!("{data:#?}");

        Ok(())
    }
}