use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Version;
pub use uuid::{self, Uuid};
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid uuid")]
InvalidUuid,
#[error("invalid username")]
InvalidUsername,
#[cfg(feature = "online")]
#[error("ureq transport error: {0}")]
Transport(ureq::Transport),
#[error("mojang api returned unexpected result")]
MojangAPIError,
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "Uuid")]
#[serde(into = "Uuid")]
pub struct OnlineUuid(Uuid);
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "Uuid")]
#[serde(into = "Uuid")]
pub struct OfflineUuid(Uuid);
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "Uuid")]
#[serde(into = "Uuid")]
pub enum PlayerUuid {
Online(OnlineUuid),
Offline(OfflineUuid),
}
#[cfg(feature = "online")]
#[derive(Deserialize)]
struct OnlineUuidResponse {
name: String,
id: PlayerUuid,
}
impl OnlineUuid {
#[cfg(feature = "online")]
pub fn get_username(&self) -> Result<String> {
let response = ureq::get(&format!(
"https://sessionserver.mojang.com/session/minecraft/profile/{}",
self.0
))
.call();
match response {
Ok(data) => {
let response: OnlineUuidResponse =
data.into_json().map_err(|_| Error::MojangAPIError)?;
Ok(response.name)
}
Err(ureq::Error::Status(_, _)) => Err(Error::InvalidUsername),
Err(ureq::Error::Transport(x)) => Err(Error::Transport(x)),
}
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.as_uuid().as_bytes()
}
}
impl OfflineUuid {
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.as_uuid().as_bytes()
}
}
impl PlayerUuid {
#[cfg(feature = "online")]
pub fn new_with_online_username(username: &str) -> Result<Self> {
let response = ureq::get(&format!(
"https://api.mojang.com/users/profiles/minecraft/{}",
username
))
.call();
match response {
Ok(data) => {
let response: OnlineUuidResponse =
data.into_json().map_err(|_| Error::MojangAPIError)?;
Ok(response.id)
}
Err(ureq::Error::Status(_, _)) => Err(Error::InvalidUsername),
Err(ureq::Error::Transport(x)) => Err(Error::Transport(x)),
}
}
#[cfg(feature = "offline")]
pub fn new_with_offline_username(username: &str) -> Self {
let mut hash = md5::compute(format!("OfflinePlayer:{}", username)).0;
hash[6] = hash[6] & 0x0f | 0x30; hash[8] = hash[8] & 0x3f | 0x80;
let uuid = Uuid::from_bytes(hash);
Self::Offline(OfflineUuid(uuid))
}
pub fn new_with_uuid(uuid: Uuid) -> Result<Self> {
match uuid.get_version() {
Some(Version::Random) => Ok(Self::Online(OnlineUuid(uuid))),
Some(Version::Md5) => Ok(Self::Offline(OfflineUuid(uuid))),
_ => Err(Error::InvalidUuid),
}
}
pub fn as_uuid(&self) -> &Uuid {
match self {
Self::Online(uuid) => uuid.as_uuid(),
Self::Offline(uuid) => uuid.as_uuid(),
}
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.as_uuid().as_bytes()
}
pub fn unwrap_offline(self) -> OfflineUuid {
match self {
Self::Online(_) => panic!("unwrap_offline called on an online uuid"),
Self::Offline(uuid) => uuid,
}
}
pub fn unwrap_online(self) -> OnlineUuid {
match self {
Self::Online(uuid) => uuid,
Self::Offline(_) => panic!("unwrap_online called on an offline uuid"),
}
}
pub fn offline(self) -> Option<OfflineUuid> {
match self {
Self::Online(_) => None,
Self::Offline(uuid) => Some(uuid),
}
}
pub fn online(self) -> Option<OnlineUuid> {
match self {
Self::Online(uuid) => Some(uuid),
Self::Offline(_) => None,
}
}
}
impl TryFrom<Uuid> for PlayerUuid {
type Error = Error;
fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
Self::new_with_uuid(value)
}
}
impl TryFrom<Uuid> for OnlineUuid {
type Error = Error;
fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
PlayerUuid::new_with_uuid(value)?
.online()
.ok_or(Error::InvalidUuid)
}
}
impl TryFrom<Uuid> for OfflineUuid {
type Error = Error;
fn try_from(value: Uuid) -> std::result::Result<Self, Self::Error> {
PlayerUuid::new_with_uuid(value)?
.offline()
.ok_or(Error::InvalidUuid)
}
}
impl From<PlayerUuid> for Uuid {
fn from(other: PlayerUuid) -> Self {
*other.as_uuid()
}
}
impl From<OnlineUuid> for Uuid {
fn from(other: OnlineUuid) -> Self {
*other.as_uuid()
}
}
impl From<OfflineUuid> for Uuid {
fn from(other: OfflineUuid) -> Self {
*other.as_uuid()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "offline")]
#[test]
fn offline_uuids() {
let values = vec![
("boolean_coercion", "db62bdfb-eddc-3acc-a14e-c703aba52549"),
("BooleanCoercion", "44d050b1-46c8-37a8-b511-7023ae304192"),
("bool", "e9fd750e-29c2-3d85-80c9-64618059d454"),
("BOOL", "c5d06acf-0ef6-3a68-bf0b-b57806bcbef5"),
("BoOl", "e38f2cf4-72d2-3a84-8278-fed6908d2746"),
("booleancoercion", "072a2e03-56ce-3960-9391-c56afe17e317"),
];
values
.into_iter()
.map(|(username, uuid)| {
(
*PlayerUuid::new_with_offline_username(username).as_uuid(),
Uuid::try_parse(uuid).unwrap(),
)
})
.for_each(|(uuid1, uuid2)| assert_eq!(uuid1, uuid2));
}
#[cfg(feature = "online")]
#[test]
fn online_uuids() {
let values = vec![
("Notch", "069a79f4-44e9-4726-a5be-fca90e38aaf5"),
("dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
("Dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
];
values
.into_iter()
.map(|(username, uuid)| {
(
*PlayerUuid::new_with_online_username(username)
.unwrap()
.as_uuid(),
Uuid::try_parse(uuid).unwrap(),
)
})
.for_each(|(uuid1, uuid2)| assert_eq!(uuid1, uuid2));
}
#[cfg(feature = "online")]
#[test]
fn online_uuids_to_names() {
let values = vec![
("Notch", "069a79f4-44e9-4726-a5be-fca90e38aaf5"),
("Dinnerbone", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"),
];
values
.into_iter()
.map(|(username, uuid)| {
(
username,
PlayerUuid::new_with_uuid(Uuid::try_parse(uuid).unwrap())
.unwrap()
.unwrap_online()
.get_username()
.unwrap(),
)
})
.for_each(|(name1, name2)| assert_eq!(name1, name2));
}
}