uuid_mc/lib.rs
1//! This library provides functionality for converting usernames to and from Minecraft UUIDs,
2//! including support for offline and online players.
3//! You may choose to disable either the `offline` or `online` features if you don't need them.
4//!
5//! To start, head over to [`PlayerUuid`] or look at some of the examples in this crate.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use uuid::Version;
10pub use uuid::{self, Uuid};
11
12/// This library's own error enum, which is returned by every function that returns a [`Result`](std::result::Result).
13#[derive(Debug, Error)]
14pub enum Error {
15 /// An error that signifies that the user has provided an invalid UUID, be it in the wrong format or a non-existent UUID if in an online context.
16 #[error("invalid uuid")]
17 InvalidUuid,
18
19 /// An error that signifies that the user has provided an invalid username to Mojang's API.
20 #[error("invalid username")]
21 InvalidUsername,
22
23 /// A Transport error from [`ureq`].
24 #[cfg(feature = "online")]
25 #[error("ureq transport error: {0}")]
26 Transport(ureq::Transport),
27
28 /// An error that signifies that the Mojang API returned an unexpected result.
29 #[error("mojang api returned unexpected result")]
30 MojangAPIError,
31}
32
33type Result<T> = std::result::Result<T, Error>;
34
35/// A struct that represents a UUID with an online format (UUID v4).
36#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
37#[serde(try_from = "Uuid")]
38#[serde(into = "Uuid")]
39pub struct OnlineUuid(Uuid);
40
41/// A struct that represents a UUID with an offline format (UUID v3).
42#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
43#[serde(try_from = "Uuid")]
44#[serde(into = "Uuid")]
45pub struct OfflineUuid(Uuid);
46
47/// An enum that can represent both kinds of UUIDs.
48#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
49#[serde(try_from = "Uuid")]
50#[serde(into = "Uuid")]
51pub enum PlayerUuid {
52 Online(OnlineUuid),
53 Offline(OfflineUuid),
54}
55
56#[cfg(feature = "online")]
57#[derive(Deserialize)]
58struct OnlineUuidResponse {
59 name: String,
60 id: PlayerUuid,
61}
62
63impl OnlineUuid {
64 /// Uses the Mojang API to fetch the username belonging to this UUID.
65 ///
66 /// # Errors
67 /// If there is no user that corresponds to the provided UUID, an [`Error::InvalidUuid`] is returned.
68 /// Otherwise, an [`Error::Transport`] can be returned in case of network failure.
69 ///
70 /// # Examples
71 /// To fetch the user belonging to an arbitrary UUID, you can do:
72 /// ```rust
73 /// use uuid::Uuid;
74 /// use uuid_mc::{PlayerUuid, OnlineUuid};
75 ///
76 /// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
77 /// let uuid = Uuid::try_parse("069a79f4-44e9-4726-a5be-fca90e38aaf5")?;
78 /// let player_uuid = PlayerUuid::new_with_uuid(uuid)?;
79 ///
80 /// let name = player_uuid.unwrap_online().get_username()?;
81 /// assert_eq!(name, "Notch");
82 /// # Ok(())
83 /// # }
84 /// ```
85 #[cfg(feature = "online")]
86 pub fn get_username(&self) -> Result<String> {
87 let response = ureq::get(&format!(
88 "https://sessionserver.mojang.com/session/minecraft/profile/{}",
89 self.0
90 ))
91 .call();
92
93 match response {
94 Ok(data) => {
95 let response: OnlineUuidResponse =
96 data.into_json().map_err(|_| Error::MojangAPIError)?;
97 Ok(response.name)
98 }
99 Err(ureq::Error::Status(_, _)) => Err(Error::InvalidUsername),
100 Err(ureq::Error::Transport(x)) => Err(Error::Transport(x)),
101 }
102 }
103
104 /// Returns the inner [Uuid].
105 pub fn as_uuid(&self) -> &Uuid {
106 &self.0
107 }
108
109 /// Returns the inner UUID, as a byte array. This is just a convenience function around `self.as_uuid().as_bytes()`.
110 pub fn as_bytes(&self) -> &[u8; 16] {
111 self.as_uuid().as_bytes()
112 }
113}
114
115impl OfflineUuid {
116 /// Returns the inner [Uuid].
117 pub fn as_uuid(&self) -> &Uuid {
118 &self.0
119 }
120
121 /// Returns the inner UUID, as a byte array. This is just a convenience function around `self.as_uuid().as_bytes()`.
122 pub fn as_bytes(&self) -> &[u8; 16] {
123 self.as_uuid().as_bytes()
124 }
125}
126
127impl PlayerUuid {
128 /// Creates a new instance using the username of an online player, by polling the Mojang API.
129 ///
130 /// # Errors
131 /// If there is no user that corresponds to the provided username, an [`Error::InvalidUsername`] is returned.
132 /// Otherwise, an [`Error::Transport`] can be returned in case of network failure.
133 ///
134 /// # Examples
135 /// To fetch the UUID of an online user:
136 /// ```rust
137 /// use uuid::Uuid;
138 /// use uuid_mc::PlayerUuid;
139 ///
140 /// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
141 /// let uuid = PlayerUuid::new_with_online_username("Notch")?;
142 /// let uuid = uuid.as_uuid();
143 /// let expected = Uuid::try_parse("069a79f4-44e9-4726-a5be-fca90e38aaf5")?;
144 /// assert_eq!(uuid, &expected);
145 /// # Ok(())
146 /// # }
147 #[cfg(feature = "online")]
148 pub fn new_with_online_username(username: &str) -> Result<Self> {
149 let response = ureq::get(&format!(
150 "https://api.mojang.com/users/profiles/minecraft/{}",
151 username
152 ))
153 .call();
154
155 match response {
156 Ok(data) => {
157 let response: OnlineUuidResponse =
158 data.into_json().map_err(|_| Error::MojangAPIError)?;
159 Ok(response.id)
160 }
161 Err(ureq::Error::Status(_, _)) => Err(Error::InvalidUsername),
162 Err(ureq::Error::Transport(x)) => Err(Error::Transport(x)),
163 }
164 }
165
166 /// Creates a new instance using the username of an offline player.
167 ///
168 /// # Examples
169 /// To fetch the UUID of an offline user:
170 /// ```rust
171 /// use uuid::Uuid;
172 /// use uuid_mc::PlayerUuid;
173 ///
174 /// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
175 /// let uuid = PlayerUuid::new_with_offline_username("boolean_coercion");
176 /// let uuid = uuid.as_uuid();
177 /// let expected = Uuid::try_parse("db62bdfb-eddc-3acc-a14e-c703aba52549")?;
178 /// assert_eq!(uuid, &expected);
179 /// # Ok(())
180 /// # }
181 #[cfg(feature = "offline")]
182 pub fn new_with_offline_username(username: &str) -> Self {
183 let mut hash = md5::compute(format!("OfflinePlayer:{}", username)).0;
184 hash[6] = hash[6] & 0x0f | 0x30; // uuid version 3
185 hash[8] = hash[8] & 0x3f | 0x80; // RFC4122 variant
186
187 let uuid = Uuid::from_bytes(hash);
188 Self::Offline(OfflineUuid(uuid))
189 }
190
191 /// Creates a new instance using an already existing [`Uuid`].
192 ///
193 /// # Errors
194 /// In case the provided Uuid is neither offline (v3) or online (v4), an [`Error::InvalidUuid`] is returned.
195 ///
196 /// # Examples
197 /// To test whether a given Uuid is of the offline or online format:
198 /// ```rust
199 /// use uuid::Uuid;
200 /// use uuid_mc::PlayerUuid;
201 ///
202 /// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
203 /// let uuid_offline = Uuid::try_parse("db62bdfb-eddc-3acc-a14e-c703aba52549")?;
204 /// let uuid_online = Uuid::try_parse("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6")?;
205 /// let player_uuid_offline = PlayerUuid::new_with_uuid(uuid_offline)?;
206 /// let player_uuid_online = PlayerUuid::new_with_uuid(uuid_online)?;
207 ///
208 /// assert!(matches!(player_uuid_offline, PlayerUuid::Offline(_)));
209 /// assert!(matches!(player_uuid_online, PlayerUuid::Online(_)));
210 /// # Ok(())
211 /// # }
212 pub fn new_with_uuid(uuid: Uuid) -> Result<Self> {
213 match uuid.get_version() {
214 Some(Version::Random) => Ok(Self::Online(OnlineUuid(uuid))),
215 Some(Version::Md5) => Ok(Self::Offline(OfflineUuid(uuid))),
216 _ => Err(Error::InvalidUuid),
217 }
218 }
219
220 /// Returns the inner [`Uuid`].
221 pub fn as_uuid(&self) -> &Uuid {
222 match self {
223 Self::Online(uuid) => uuid.as_uuid(),
224 Self::Offline(uuid) => uuid.as_uuid(),
225 }
226 }
227
228 /// Returns the inner UUID, as a byte array. This is just a convenience function around `self.as_uuid().as_bytes()`.
229 pub fn as_bytes(&self) -> &[u8; 16] {
230 self.as_uuid().as_bytes()
231 }
232
233 /// Similar to [`Result::unwrap`](std::result::Result::unwrap), this function returns the inner [`OfflineUuid`].
234 ///
235 /// # Panics
236 /// If the inner UUID is not an offline one.
237 pub fn unwrap_offline(self) -> OfflineUuid {
238 match self {
239 Self::Online(_) => panic!("unwrap_offline called on an online uuid"),
240 Self::Offline(uuid) => uuid,
241 }
242 }
243
244 /// Similar to [`Result::unwrap`](std::result::Result::unwrap), this function returns the inner [`OnlineUuid`].
245 ///
246 /// # Panics
247 /// If the inner UUID is not an online one.
248 pub fn unwrap_online(self) -> OnlineUuid {
249 match self {
250 Self::Online(uuid) => uuid,
251 Self::Offline(_) => panic!("unwrap_online called on an offline uuid"),
252 }
253 }
254
255 /// Returns the contained [`OfflineUuid`] if it is present, or [`None`] otherwise.
256 pub fn offline(self) -> Option<OfflineUuid> {
257 match self {
258 Self::Online(_) => None,
259 Self::Offline(uuid) => Some(uuid),
260 }
261 }
262
263 /// Returns the contained [`OnlineUuid`] if it is present, or [`None`] otherwise.
264 pub fn online(self) -> Option<OnlineUuid> {
265 match self {
266 Self::Online(uuid) => Some(uuid),
267 Self::Offline(_) => None,
268 }
269 }
270}
271
272impl TryFrom<Uuid> for PlayerUuid {
273 type Error = Error;
274
275 fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
276 Self::new_with_uuid(value)
277 }
278}
279
280impl TryFrom<Uuid> for OnlineUuid {
281 type Error = Error;
282
283 fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
284 PlayerUuid::new_with_uuid(value)?
285 .online()
286 .ok_or(Error::InvalidUuid)
287 }
288}
289
290impl TryFrom<Uuid> for OfflineUuid {
291 type Error = Error;
292
293 fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
294 PlayerUuid::new_with_uuid(value)?
295 .offline()
296 .ok_or(Error::InvalidUuid)
297 }
298}
299
300impl From<PlayerUuid> for Uuid {
301 fn from(other: PlayerUuid) -> Self {
302 *other.as_uuid()
303 }
304}
305
306impl From<OnlineUuid> for Uuid {
307 fn from(other: OnlineUuid) -> Self {
308 *other.as_uuid()
309 }
310}
311
312impl From<OfflineUuid> for Uuid {
313 fn from(other: OfflineUuid) -> Self {
314 *other.as_uuid()
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[cfg(feature = "offline")]
323 #[test]
324 fn offline_uuids() {
325 let values = vec![
326 ("boolean_coercion", "db62bdfb-eddc-3acc-a14e-c703aba52549"),
327 ("BooleanCoercion", "44d050b1-46c8-37a8-b511-7023ae304192"),
328 ("bool", "e9fd750e-29c2-3d85-80c9-64618059d454"),
329 ("BOOL", "c5d06acf-0ef6-3a68-bf0b-b57806bcbef5"),
330 ("BoOl", "e38f2cf4-72d2-3a84-8278-fed6908d2746"),
331 ("booleancoercion", "072a2e03-56ce-3960-9391-c56afe17e317"),
332 ];
333
334 values
335 .into_iter()
336 .map(|(username, uuid)| {
337 (
338 *PlayerUuid::new_with_offline_username(username).as_uuid(),
339 Uuid::try_parse(uuid).unwrap(),
340 )
341 })
342 .for_each(|(uuid1, uuid2)| assert_eq!(uuid1, uuid2));
343 }
344
345 #[cfg(feature = "online")]
346 #[test]
347 fn online_uuids() {
348 let values = vec![
349 ("Notch", "069a79f4-44e9-4726-a5be-fca90e38aaf5"),
350 ("dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
351 ("Dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
352 ];
353
354 values
355 .into_iter()
356 .map(|(username, uuid)| {
357 (
358 *PlayerUuid::new_with_online_username(username)
359 .unwrap()
360 .as_uuid(),
361 Uuid::try_parse(uuid).unwrap(),
362 )
363 })
364 .for_each(|(uuid1, uuid2)| assert_eq!(uuid1, uuid2));
365 }
366
367 #[cfg(feature = "online")]
368 #[test]
369 fn online_uuids_to_names() {
370 let values = vec![
371 ("Notch", "069a79f4-44e9-4726-a5be-fca90e38aaf5"),
372 ("Dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
373 ];
374
375 values
376 .into_iter()
377 .map(|(username, uuid)| {
378 (
379 username,
380 PlayerUuid::new_with_uuid(Uuid::try_parse(uuid).unwrap())
381 .unwrap()
382 .unwrap_online()
383 .get_username()
384 .unwrap(),
385 )
386 })
387 .for_each(|(name1, name2)| assert_eq!(name1, name2));
388 }
389}