Skip to main content

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}