summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-02-13 14:54:31 -0600
committerLibravatar bigfoot547 <[email protected]>2025-02-13 14:54:31 -0600
commitae9727f42c7059dd1ae751411dded1819c6c0a11 (patch)
tree7de68aa1d4d02831a7fa3f558afd197aff182332
parentminecraft (diff)
overhaul auth errors
-rw-r--r--ozone-cli/src/main.rs64
-rw-r--r--ozone/src/auth.rs106
-rw-r--r--ozone/src/auth/error.rs133
-rw-r--r--ozone/src/auth/mcservices.rs19
-rw-r--r--ozone/src/auth/msa.rs24
-rw-r--r--ozone/src/auth/types.rs20
6 files changed, 234 insertions, 132 deletions
diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs
index c53eade..4a8f368 100644
--- a/ozone-cli/src/main.rs
+++ b/ozone-cli/src/main.rs
@@ -1,6 +1,7 @@
mod cli;
use std::error::Error;
+use std::path::Path;
use std::process::ExitCode;
use log::{error, info, trace, LevelFilter};
use clap::Parser;
@@ -10,12 +11,42 @@ use uuid::Uuid;
use ozone::auth::{Account, AuthenticationDatabase};
use crate::cli::{Cli, InstanceCommand, RootCommand};
-fn find_account(auth: &AuthenticationDatabase, input: &str) -> Option<Account> {
- for user in auth.users.iter() {
-
+fn find_account<'a>(auth: &'a AuthenticationDatabase, input: &str) -> Vec<&'a Account> {
+ if let Ok(uuid) = input.parse::<Uuid>() {
+ todo!()
+ }
+
+ let input = input.to_ascii_lowercase();
+
+ auth.users.iter().filter(|a| match *a {
+ Account::Dummy(profile) => profile.name.to_ascii_lowercase().starts_with(&input),
+ Account::MSA(account) =>
+ account.player_profile.as_ref().is_some_and(|p| p.name.to_ascii_lowercase().starts_with(&input)) ||
+ account.gamertag.as_ref().is_some_and(|p| p.to_ascii_lowercase().starts_with(&input))
+ }).collect()
+}
+
+fn display_instance(instance: &Instance, id: Uuid, home: impl AsRef<Path>, selected: bool, verbose: bool) {
+ println!("Instance `{}':{}", instance.name, if selected { " (selected)" } else { "" });
+ println!(" UUID: {}", id);
+ println!(" Version: {}", instance.game_version);
+ println!(" Location: {}", home.as_ref().join(Settings::get_instance_path(id)).display());
+
+ if !verbose { return; }
+
+ if let Some(ref args) = instance.jvm_arguments {
+ println!(" JVM arguments: <{} argument{}>", args.len(), if args.len() == 1 { "" } else { "s" })
+ }
+
+ if let Some(res) = instance.resolution {
+ println!(" Resolution: {}x{}", res.width, res.height);
+ }
+
+ match &instance.java_runtime {
+ Some(JavaRuntimeSetting::Component(c)) => println!(" Java runtime component: {c}"),
+ Some(JavaRuntimeSetting::Path(p)) => println!(" Java runtime path: {}", p.display()),
+ _ => ()
}
-
- todo!()
}
async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
@@ -45,12 +76,8 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
first = false;
let cur_id = *cur_id;
- let sel = if settings.selected_instance.is_some_and(|id| id == cur_id) { " (selected)" } else { "" };
-
- println!("Instance `{}'{sel}:", instance.name);
- println!(" UUID: {}", cur_id);
- println!(" Version: {}", instance.game_version);
- println!(" Location: {}", home.join(Settings::get_instance_path(cur_id)).display());
+ let sel = settings.selected_instance.is_some_and(|id| id == cur_id);
+ display_instance(instance, cur_id, &home, sel, false);
}
},
InstanceCommand::Create(args) => {
@@ -158,18 +185,7 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
return Ok(ExitCode::FAILURE);
};
- println!("Information for selected profile `{}' ({}):", inst.name, settings.selected_instance.unwrap());
- println!(" Version: {}", inst.game_version);
-
- match &inst.java_runtime {
- Some(JavaRuntimeSetting::Component(c)) => println!(" Java runtime component: {c}"),
- Some(JavaRuntimeSetting::Path(p)) => println!(" Java runtime path: {}", p.display()),
- _ => ()
- }
-
- if let Some(res) = inst.resolution {
- println!(" Game resolution: {}x{}", res.width, res.height);
- }
+ display_instance(inst, settings.selected_instance.unwrap(), &home, false, true);
},
InstanceCommand::Rename { name } => {
if name.is_empty() {
@@ -182,7 +198,7 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> {
return Ok(ExitCode::FAILURE);
};
- inst.name.replace_range(.., &name);
+ inst.name.replace_range(.., name);
settings.save().await?;
}
},
diff --git a/ozone/src/auth.rs b/ozone/src/auth.rs
index 0ca96ad..cb905af 100644
--- a/ozone/src/auth.rs
+++ b/ozone/src/auth.rs
@@ -1,75 +1,21 @@
mod types;
mod msa;
mod mcservices;
+pub mod error;
-use std::error::Error;
-use std::fmt::{Display, Formatter};
use std::future::Future;
-use std::time::{Duration, SystemTime};
+use std::time::Duration;
use chrono::{DateTime, TimeDelta, Utc};
use log::debug;
-use oauth2::{AccessToken, DeviceAuthorizationUrl, DeviceCodeErrorResponse, EndpointNotSet, EndpointSet, HttpClientError, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardRevocableToken, TokenResponse, TokenUrl};
+use oauth2::{AccessToken, DeviceAuthorizationUrl, EndpointNotSet, EndpointSet, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardRevocableToken, TokenResponse, TokenUrl};
use oauth2::basic::{BasicErrorResponse, BasicErrorResponseType, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenResponse};
use reqwest::{IntoUrl, Method, RequestBuilder};
+
pub use types::*;
+use crate::auth::error::{AuthError, AuthErrorKind};
use self::msa::{XSTS_RP_MINECRAFT_SERVICES, XSTS_RP_XBOX_LIVE};
use crate::util;
-#[derive(Debug)]
-pub enum AuthError {
- // An unexpected error happened while performing a request
- Request { what: &'static str, error: reqwest::Error },
- OAuthRequestToken { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, BasicErrorResponse> },
- OAuthRequestDeviceCode { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse> },
-
- // Some internal auth error (unrecoverable)
- Internal(String),
-
- // Device code auth was cancelled
- Cancel(Option<Box<dyn Error>>),
-
- // Device code auth timed out
- Timeout,
-
- // Requires interactive authentication
- RequireInteractive(&'static str),
-
- // XSTS error
- AuthXError { what: &'static str, x_error: u64, message: Option<String> },
-
- // You don't own the game!
- EntitlementError
-}
-
-impl Display for AuthError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- AuthError::Request { what, error } => write!(f, "auth request error ({}): {}", what, error),
- AuthError::OAuthRequestToken { what, error } => write!(f, "oauth error requesting token ({what}): {error}"),
- AuthError::OAuthRequestDeviceCode { what, error } => write!(f, "oauth error with device code ({what}): {error}"),
- AuthError::Internal(msg) => write!(f, "internal auth error: {}", msg),
- AuthError::Cancel(Some(error)) => write!(f, "operation cancelled: {error}"),
- AuthError::Cancel(None) => f.write_str("operation cancelled"),
- AuthError::Timeout => f.write_str("interactive authentication timed out"),
- AuthError::RequireInteractive(why) => write!(f, "user must log in interactively: {why}"),
- AuthError::AuthXError { what, x_error, message } => write!(f, "XSTS error: {what} ({x_error} -> {})", message.as_ref().map_or("<no message>", |s| s.as_str())),
- AuthError::EntitlementError => f.write_str("no minecraft entitlement (do you own the game?)")
- }
- }
-}
-
-impl Error for AuthError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- AuthError::Request { error, .. } => Some(error),
- AuthError::OAuthRequestToken { error, .. } => Some(error),
- AuthError::OAuthRequestDeviceCode { error, .. } => Some(error),
- AuthError::Cancel(Some(error)) => Some(error.as_ref()),
- _ => None
- }
- }
-}
-
impl Token {
fn is_expired(&self, now: DateTime<Utc>) -> bool {
self.expire.is_some_and(|exp| now >= exp)
@@ -110,9 +56,9 @@ impl MsaAccount {
let to_scope = |f: &&str| Scope::new(String::from(*f));
if self.is_azure_client_id {
- AZURE_LOGIN_SCOPES.iter().map(to_scope)
+ AZURE_LOGIN_SCOPES.into_iter().map(to_scope)
} else {
- NON_AZURE_LOGIN_SCOPES.iter().map(to_scope)
+ NON_AZURE_LOGIN_SCOPES.into_iter().map(to_scope)
}
}
@@ -136,7 +82,14 @@ impl MsaAccount {
.add_scopes(self.scopes_iter())
.add_extra_param("response_type", "device_code")
.request_async(client)
- .await.map_err(|e| AuthError::OAuthRequestToken { what: "refresh", error: e })?;
+ .await.map_err(|e| match e {
+ RequestTokenError::ServerResponse(res)
+ if match res.error() {
+ BasicErrorResponseType::Extension(s) if s == "interaction_required" || s == "consent_required" => true,
+ _ => false
+ } => AuthError::raw_msg(AuthErrorKind::InteractionRequired, "microsoft requested interactive login"),
+ _ => AuthError::internal_msg_src("refresh", e)
+ })?;
self.refresh_token = tokenres.refresh_token().cloned();
@@ -154,14 +107,14 @@ impl MsaAccount {
.add_scopes(self.scopes_iter())
.add_extra_param("response_type", "device_code")
.request_async(client)
- .await.map_err(|e| AuthError::OAuthRequestToken { what: "device code", error: e })?;
+ .await.map_err(|e| AuthError::internal_msg_src("device code", e))?;
handle_device(device_auth.clone()).await;
let tokenres = oauth_client.exchange_device_access_token(&device_auth)
.set_max_backoff_interval(Duration::from_secs(20u64))
.request_async(client, tokio::time::sleep, None)
- .await.map_err(|e| AuthError::OAuthRequestDeviceCode { what: "device access code", error: e })?;
+ .await.map_err(|e| AuthError::internal_msg_src("device access code", e))?;
self.refresh_token = tokenres.refresh_token().cloned();
@@ -180,20 +133,11 @@ impl MsaAccount {
}
if self.refresh_token.is_none() {
- return Err(AuthError::RequireInteractive("no refresh token"));
+ return Err(AuthError::raw_msg(AuthErrorKind::InteractionRequired, "refresh token missing"));
}
debug!("XBL token expired. Trying to refresh it.");
- self.xbl_login_refresh(client).await
- .map_err(|e| match &e {
- AuthError::OAuthRequestToken { error: RequestTokenError::ServerResponse(res), .. } => match res.error() {
- BasicErrorResponseType::Extension(s) if s == "interaction_required" || s == "consent_required" => {
- AuthError::RequireInteractive("msa requested interactive logon")
- },
- _ => e
- },
- _ => e
- })?;
+ self.xbl_login_refresh(client).await?;
self.mc_token = None;
@@ -215,7 +159,7 @@ impl MsaAccount {
let (user_hash, mc_xsts_tok) = match msa::xsts_request(client, xbl_token, XSTS_RP_MINECRAFT_SERVICES).await? {
msa::XSTSAuthResponse::Success(res) => {
let user_hash = res.get_user_hash()
- .map_or(Err(AuthError::Internal("malformed response: no user hash".into())), |h| Ok(h.to_owned()))?;
+ .map_or(Err(AuthError::internal_msg("malformed response: no user hash")), |h| Ok(h.to_owned()))?;
(user_hash, res.into_token())
},
msa::XSTSAuthResponse::Error(e) => return Err(e.into())
@@ -237,7 +181,7 @@ impl MsaAccount {
};
let Some(xuid) = res.get_xuid() else {
- return Err(AuthError::Internal("missing xuid for user".into()));
+ return Err(AuthError::internal_msg("missing xuid for user"));
};
self.xuid = Some(xuid.to_owned());
@@ -255,13 +199,13 @@ impl MsaAccount {
debug!("Checking if you own the game...");
if !mcservices::owns_the_game(client, mc_token).await? {
- return Err(AuthError::EntitlementError);
+ return Err(AuthErrorKind::NotEntitled.into());
}
debug!("Getting your profile info...");
let player_info = mcservices::get_player_info(client, mc_token).await?;
let player_profile = mcservices::get_player_profile(client, player_info.id).await
- .map_err(|e| AuthError::Request { what: "looking up profile", error: e })?;
+ .map_err(|e| AuthError::internal_msg_src("looking up profile", e))?;
self.player_info = Some(player_info);
self.player_profile = Some(player_profile);
@@ -315,8 +259,8 @@ mod test {
loop {
match user.log_in_silent(&client).await {
Ok(_) => break,
- Err(AuthError::RequireInteractive(s)) => {
- debug!("Requires interactive auth: {s}")
+ Err(e) if e.kind() == AuthErrorKind::InteractionRequired => {
+ debug!("Requires interactive auth: {e}")
},
Err(e) => {
panic!("{}", e);
diff --git a/ozone/src/auth/error.rs b/ozone/src/auth/error.rs
new file mode 100644
index 0000000..da281c5
--- /dev/null
+++ b/ozone/src/auth/error.rs
@@ -0,0 +1,133 @@
+use std::error::Error;
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AuthErrorKind {
+ Internal, // an internal error occurred
+
+ Timeout, // interactive authentication timed out
+ Cancel, // interactive authentication cancelled by user code
+ InteractionRequired, // interactive authentication required
+
+ NotOnXbox, // user is not on xbox
+ TooYoung, // user is too young and not in a family
+ NotEntitled, // user doesn't own Minecraft
+ NoProfile, // user owns the game but has not yet created a profile
+}
+
+#[derive(Debug)]
+pub struct AuthError {
+ pub(super) kind: AuthErrorKind,
+ pub(super) message: Option<String>,
+ pub(super) source: Option<Box<dyn Error>>
+}
+
+macro_rules! error_kind_messages {
+ ($ty:ident, $({ $name:ident, $value:literal } $(,)?)* $({ _, $def_value:literal })?) => {
+ impl Display for $ty {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match *self {
+ $($ty::$name => f.write_str($value)),+,
+ $(_ => f.write_str($def_value))?
+ }
+ }
+ }
+ }
+}
+
+error_kind_messages!(AuthErrorKind,
+ { Internal, "internal authentication error" },
+
+ { Timeout, "interactive authentication timed out" },
+ { Cancel, "interactive authentication cancelled by user code" },
+ { InteractionRequired, "interactive authentication required" },
+
+ { NotOnXbox, "user is not on xbox" },
+ { TooYoung, "user is too young" },
+ { NotEntitled, "user does not own the game" },
+ { NoProfile, "user has not created a profile" }
+);
+
+impl AuthError {
+ pub fn internal_msg(message: impl Into<String>) -> AuthError {
+ Self::raw_msg(AuthErrorKind::Internal, message.into())
+ }
+
+ pub fn internal_src<E: Into<Box<dyn Error>>>(error: E) -> AuthError {
+ Self::raw_src(AuthErrorKind::Internal, error)
+ }
+
+ pub fn internal_msg_src<E: Into<Box<dyn Error>>>(message: impl Into<String>, source: E) -> AuthError {
+ Self::raw_msg_src(AuthErrorKind::Internal, message, source)
+ }
+
+ pub fn raw_msg_src<E, S>(kind: AuthErrorKind, message: S, source: E) -> AuthError
+ where
+ E: Into<Box<dyn Error>>,
+ S: Into<String>
+ {
+ AuthError {
+ kind,
+ message: Some(message.into()),
+ source: Some(source.into())
+ }
+ }
+
+ pub fn raw_src<E>(kind: AuthErrorKind, source: E) -> AuthError
+ where
+ E: Into<Box<dyn Error>>
+ {
+ AuthError {
+ kind,
+ message: None,
+ source: Some(source.into())
+ }
+ }
+
+ pub fn raw_msg<S>(kind: AuthErrorKind, message: S) -> AuthError
+ where
+ S: Into<String>
+ {
+ AuthError {
+ kind,
+ message: Some(message.into()),
+ source: None
+ }
+ }
+
+ pub fn kind(&self) -> AuthErrorKind {
+ self.kind
+ }
+
+ pub fn message(&self) -> Option<&str> {
+ self.message.as_deref()
+ }
+}
+
+impl From<AuthErrorKind> for AuthError {
+ fn from(kind: AuthErrorKind) -> Self {
+ Self { kind, message: None, source: None }
+ }
+}
+
+impl Display for AuthError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ if let Some(ref msg) = self.message {
+ write!(f, "{}: {msg}", self.kind)?;
+ } else {
+ write!(f, "{}", self.kind)?;
+ }
+
+ if let Some(ref source) = self.source {
+ write!(f, " - caused by: {source}")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl Error for AuthError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ self.source.as_deref()
+ }
+}
diff --git a/ozone/src/auth/mcservices.rs b/ozone/src/auth/mcservices.rs
index 45ef795..588a833 100644
--- a/ozone/src/auth/mcservices.rs
+++ b/ozone/src/auth/mcservices.rs
@@ -1,8 +1,9 @@
use std::time::{Duration, SystemTime};
use chrono::{DateTime, Utc};
-use reqwest::{IntoUrl, Method, RequestBuilder};
+use reqwest::{IntoUrl, Method, RequestBuilder, StatusCode};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
+use crate::auth::error::AuthErrorKind;
use super::{AuthError, MinecraftPlayerInfo, PlayerProfile};
use super::types::Token;
@@ -40,9 +41,9 @@ pub async fn login_with_xbox(client: &reqwest::Client, xsts_token: &str, user_ha
let res: MinecraftXboxLoginResponse = super::build_json_request(client, MINECRAFT_LOGIN, Method::POST)
.json(&req).send().await
.and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "minecraft xbox login", error: e })?
+ .map_err(|e| AuthError::internal_msg_src("minecraft xbox login", e))?
.json().await
- .map_err(|e| AuthError::Request { what: "minecraft xbox login (decode)", error: e })?;
+ .map_err(|e| AuthError::internal_msg_src("minecraft xbox login (decode)", e))?;
let now: DateTime<Utc> = SystemTime::now().into();
@@ -68,9 +69,9 @@ pub async fn owns_the_game(client: &reqwest::Client, token: &str) -> Result<bool
let res: EntitlementResponse = build_authenticated(client, MINECRAFT_ENTITLEMENTS, Method::GET, token)
.send().await
.and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "entitlements", error: e })?
+ .map_err(|e| AuthError::internal_msg_src("entitlements", e))?
.json().await
- .map_err(|e| AuthError::Request { what: "entitlements (receive)", error: e})?;
+ .map_err(|e| AuthError::internal_msg_src("entitlements (receive)", e))?;
Ok(res.items.iter().any(|i| i.name == "game_minecraft" || i.name == "product_minecraft"))
}
@@ -79,9 +80,13 @@ pub async fn get_player_info(client: &reqwest::Client, token: &str) -> Result<Mi
build_authenticated(client, MINECRAFT_PROFILE, Method::GET, token)
.send().await
.and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "player info", error: e })?
+ .map_err(|e| if e.status().is_some_and(|s| s == StatusCode::NOT_FOUND) {
+ AuthErrorKind::NoProfile.into()
+ } else {
+ AuthError::internal_msg_src("player info (connect)", e)
+ })?
.json().await
- .map_err(|e| AuthError::Request { what: "player info (receive)", error: e })
+ .map_err(|e| AuthError::internal_msg_src("player info (receive)", e))
}
pub async fn get_player_profile(client: &reqwest::Client, uuid: Uuid) -> Result<PlayerProfile, reqwest::Error> {
diff --git a/ozone/src/auth/msa.rs b/ozone/src/auth/msa.rs
index ac4a75b..0c84a7a 100644
--- a/ozone/src/auth/msa.rs
+++ b/ozone/src/auth/msa.rs
@@ -5,7 +5,7 @@ use log::debug;
use oauth2::AccessToken;
use reqwest::{Method};
use serde::{Deserialize, Serialize};
-use crate::auth::AuthError;
+use crate::auth::error::{AuthError, AuthErrorKind};
use crate::auth::types::Token;
const XBOX_LIVE_AUTH: &str = "https://user.auth.xboxlive.com/user/authenticate";
@@ -54,8 +54,8 @@ pub async fn xbox_live_login(client: &reqwest::Client, access_token: &AccessToke
let res: XboxLiveAuthResponse = super::build_json_request(client, XBOX_LIVE_AUTH, Method::POST).json(&request).send().await
.and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "xbox live auth", error: e })?
- .json().await.map_err(|e| AuthError::Request { what: "xbox live auth (decode)", error: e })?;
+ .map_err(|e| AuthError::internal_msg_src("xbox live authentication (connect)", e))?
+ .json().await.map_err(|e| AuthError::internal_msg_src("xbox live authentication (receive)", e))?;
Ok(Token {
value: res.token,
@@ -130,15 +130,11 @@ impl XSTSAuthSuccessResponse {
#[allow(clippy::from_over_into)]
impl Into<AuthError> for XSTSAuthErrorResponse {
fn into(self) -> AuthError {
- AuthError::AuthXError {
- // some well-known error values
- what: match self.x_err {
- 2148916238u64 => "Microsoft account held by a minor outside of a family.",
- 2148916233u64 => "Account is not on Xbox.",
- _ => "Unknown error."
- },
- x_error: self.x_err,
- message: self.message
+ match self.x_err {
+ 2148916238u64 => AuthErrorKind::TooYoung.into(),
+ 2148916233u64 => AuthErrorKind::NotOnXbox.into(),
+ _ => AuthError::raw_msg(AuthErrorKind::Internal,
+ format!("unknown xsts error: ({}) {}", self.x_err, self.message.as_deref().unwrap_or("<no message>")))
}
}
}
@@ -161,9 +157,9 @@ pub async fn xsts_request(client: &reqwest::Client, xbl_token: &str, relying_par
let res: XSTSAuthResponse = super::build_json_request(client, XBOX_LIVE_XSTS, Method::POST).json(&req).send().await
.and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "xsts", error: e })?
+ .map_err(|e| AuthError::internal_msg_src("xsts error (connect)", e))?
.json().await
- .map_err(|e| AuthError::Request { what: "xsts (decode)", error: e })?;
+ .map_err(|e| AuthError::internal_msg_src("xsts error (receive)", e))?;
Ok(res)
}
diff --git a/ozone/src/auth/types.rs b/ozone/src/auth/types.rs
index a4eeba1..fa73c45 100644
--- a/ozone/src/auth/types.rs
+++ b/ozone/src/auth/types.rs
@@ -1,7 +1,7 @@
pub mod property_map;
pub use property_map::PropertyMap;
-use std::fmt::{Debug, Formatter};
+use std::fmt::{Debug, Display, Formatter};
use chrono::{DateTime, Utc};
use multimap::MultiMap;
use oauth2::RefreshToken;
@@ -18,6 +18,7 @@ pub struct Property {
}
#[derive(Debug, Serialize, Deserialize)]
+#[non_exhaustive]
pub struct PlayerProfile {
#[serde(with = "uuid::serde::simple")]
pub id: Uuid,
@@ -25,9 +26,6 @@ pub struct PlayerProfile {
#[serde(default, skip_serializing_if = "MultiMap::is_empty", with = "property_map")]
pub properties: PropertyMap,
-
- #[serde(skip)]
- _private: ()
}
impl PlayerProfile {
@@ -38,8 +36,7 @@ impl PlayerProfile {
PlayerProfile {
id: uuid,
name,
- properties: Default::default(),
- _private: ()
+ properties: Default::default()
}
}
}
@@ -145,3 +142,14 @@ pub enum Account {
pub struct AuthenticationDatabase {
pub users: Vec<Account>
}
+
+impl Display for Account {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Account::Dummy(profile) => write!(f, "[dummy] {}", profile.name),
+ Account::MSA(account) => write!(f, "[msa] {} ({})",
+ account.gamertag.as_deref().unwrap_or("???"),
+ account.player_profile.as_ref().map(|p| p.name.as_str()).unwrap_or("???"))
+ }
+ }
+}