From 749bacadc4425797c695f815dd5dfe63ba30cde7 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Thu, 13 Feb 2025 21:40:26 -0600 Subject: cli account management --- ozone-cli/src/cli.rs | 24 ++++++- ozone-cli/src/main.rs | 192 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 205 insertions(+), 11 deletions(-) (limited to 'ozone-cli/src') diff --git a/ozone-cli/src/cli.rs b/ozone-cli/src/cli.rs index 6ba23d3..8ed2f33 100644 --- a/ozone-cli/src/cli.rs +++ b/ozone-cli/src/cli.rs @@ -176,11 +176,33 @@ pub struct ProfileSelectArgs { pub gamertag: Option } +#[derive(Args, Debug)] +pub struct ProfileSignInArgs { + /// Elect to use an alternate client id to authenticate to Minecraft. + /// + /// Avoid using this if possible, + /// since you will not be able to easily revoke oauth access to the launcher. + /// + /// Situations when you might need to use this are if your Microsoft account's birthday + /// is set to a date fewer than 18 years in the past while not in a family. + #[arg(long)] + pub use_alt_client_id: bool, + + #[arg(long)] + pub no_select: bool +} + #[derive(Subcommand, Debug)] pub enum AccountCommand { Select(ProfileSelectArgs), Forget, - SignIn + + #[command(name = "signin")] + SignIn(ProfileSignInArgs), + + List, + Info, + Refresh } #[derive(Args, Debug)] diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index 62f7155..f033a19 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -5,22 +5,23 @@ use std::path::Path; use std::process::ExitCode; use log::{error, info, trace, LevelFilter}; use clap::Parser; -use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings}; +use ozone::launcher::{Instance, JavaRuntimeSetting, Launcher, Settings, ALT_CLIENT_ID, MAIN_CLIENT_ID}; use ozone::launcher::version::{VersionList, VersionResult}; use uuid::Uuid; -use ozone::auth::{Account, AuthenticationDatabase}; +use ozone::auth::{Account, AccountStorage, MsaAccount}; +use ozone::auth::error::AuthErrorKind; use crate::cli::{AccountCommand, Cli, InstanceCommand, ProfileSelectArgs, RootCommand}; -fn find_account<'a>(auth: &'a AuthenticationDatabase, input: &ProfileSelectArgs) -> Result, ()> { +fn find_account<'a>(auth: &'a AccountStorage, input: &ProfileSelectArgs) -> Result, ()> { if let Some(uuid) = input.uuid { - Ok(auth.users.iter().filter(|a| match *a { + Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::Dummy(p) => p.id == uuid, Account::MSA(account) => account.player_profile.as_ref().is_some_and(|p| p.id == uuid) }).collect()) } else if let Some(ref name) = input.name { let name = name.to_ascii_lowercase(); - Ok(auth.users.iter().filter(|a| match *a { + Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::Dummy(profile) => profile.name.to_ascii_lowercase().starts_with(&name), Account::MSA(account) => account.player_profile.as_ref().is_some_and(|p| p.name.to_ascii_lowercase().starts_with(&name)) @@ -28,12 +29,12 @@ fn find_account<'a>(auth: &'a AuthenticationDatabase, input: &ProfileSelectArgs) } else if let Some(ref gt) = input.gamertag { let gt = gt.to_ascii_lowercase(); - Ok(auth.users.iter().filter(|a| match *a { + Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::MSA(account) => account.gamertag.as_ref().is_some_and(|g| g.to_ascii_lowercase().starts_with(>)), _ => false }).collect()) } else if let Some(ref xuid) = input.xuid { - Ok(auth.users.iter().filter(|a| match *a { + Ok(auth.iter_accounts().filter(|(_, a)| match *a { Account::MSA(account) => account.xuid.as_ref().is_some_and(|x| x == xuid), _ => false }).collect()) @@ -66,6 +67,45 @@ fn display_instance(instance: &Instance, id: Uuid, home: impl AsRef, selec } } +fn display_account(account: &Account, selected: bool, verbose: bool) { + let selected = if selected { " (selected)" } else { "" }; + + match *account { + Account::Dummy(ref profile) => { + println!("Dummy account:{selected}"); + println!(" Username: {}", profile.name); + println!(" UUID: {}", profile.id); + if verbose { + println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" }); + } + }, + Account::MSA(ref msa_acct) => { + println!("Microsoft account:{selected}"); + + if let Some(ref profile) = msa_acct.player_profile { + println!(" Username: {}", profile.name); + println!(" UUID: {}", profile.id); + + if verbose { + println!(" Properties: <{} propert{}>", profile.properties.len(), if profile.properties.len() == 1 { "y" } else { "ies" }); + } + } else { + println!(" Username: "); + println!(" UUID: "); + + if verbose { + println!(" Properties: "); + } + } + + println!(" Xbox Gamertag: {}", msa_acct.gamertag.as_deref().unwrap_or("")); + if verbose { + println!(" XUID: {}", msa_acct.xuid.as_deref().unwrap_or("")); + } + } + } +} + async fn main_inner(cli: Cli) -> Result> { let Some(home) = cli.home.or_else(Launcher::sensible_home) else { error!("Could not choose a launcher home directory. Please choose one with `--home'."); @@ -81,7 +121,7 @@ async fn main_inner(cli: Cli) -> Result> { let mut first = true; if settings.instances.is_empty() { - eprintln!("There are no instances. Create one with `profile create '."); + eprintln!("There are no instances. Create one with `instance create '."); return Ok(ExitCode::SUCCESS); } @@ -220,17 +260,149 @@ async fn main_inner(cli: Cli) -> Result> { } }, RootCommand::Account(account_args) => { - // TODO: load auth db and do stuff with it + let accounts_path = home.join("ozone_accounts.json"); + let mut accounts = AccountStorage::load(&accounts_path).await?; match &account_args.command { AccountCommand::Select(args) => { + let Ok(results) = find_account(&accounts, args) else { + return Ok(ExitCode::FAILURE); + }; + + if results.is_empty() { + eprintln!("No account was found."); + return Ok(ExitCode::FAILURE); + } + + if results.len() > 1 { + eprintln!("Ambiguous argument. Found {} accounts:", results.len()); + for (_, account) in results { + eprintln!("- {account}"); + } + return Ok(ExitCode::FAILURE); + } + + let (key, account) = results.into_iter().next().unwrap(); + println!("Selected account: {account}"); + let key = key.clone(); + accounts.set_selected_account(key); + accounts.save(&accounts_path).await?; }, AccountCommand::Forget => { + if accounts.pop_selected_account().is_none() { + eprintln!("No account selected."); + return Ok(ExitCode::FAILURE); + } + + accounts.save(&accounts_path).await?; + }, + AccountCommand::SignIn(args) => { + let (client_id, azure) = if args.use_alt_client_id { + (ALT_CLIENT_ID, false) + } else { + (MAIN_CLIENT_ID, true) + }; + + let client = MsaAccount::create_client(); + + let mut acct = MsaAccount::with_client_id(client_id, azure); + acct.xbl_login_device(&client, |d| async move { + if let Some(uri_complete) = d.verification_uri_complete() { + println!("In a browser, please navigate to the following URL:"); + println!("{}", uri_complete.secret()); + } else { + println!("In a browser, please navigate to the following URL: {}", d.verification_uri()); + println!("Use the following device code: {}", d.user_code().secret()) + } + }).await?; + + println!("Authentication success! Logging in..."); + + match acct.log_in_silent(&client).await { + Ok(_) => (), + Err(e) => match e.kind() { + AuthErrorKind::NotOnXbox => { + eprintln!("This Microsoft account is not on Xbox. Please make sure you are using the correct Microsoft account."); + return Ok(ExitCode::FAILURE); + }, + AuthErrorKind::TooYoung => { + eprintln!("Currently, Microsoft accounts held by minors (under 18 years old) cannot sign into third party applications (such as olauncher) unless they are in a family."); + eprintln!("If you do not wish to configure a family, try running this command with the `--use-alt-client-id' flag."); + return Ok(ExitCode::FAILURE); + }, + AuthErrorKind::NotEntitled => { + eprintln!("Warning: This Microsoft account does not seem to own the game."); + eprintln!("This account will only be able to play the demo version of the game."); + }, + AuthErrorKind::NoProfile => { + eprintln!("Warning: It appears that you own the game but have not yet created a profile."); + eprintln!("Visit https://minecraft.net to choose a name. Until then, you will only be able to play the demo version of the game."); + }, + _ => { + eprintln!("An unknown error occurred while signing into the account:"); + eprintln!("{e}"); + return Ok(ExitCode::FAILURE); + } + } + } + + let key = accounts.add_account(acct.into()).expect("authentication succeeded but xuid missing????"); + if !args.no_select { + accounts.set_selected_account(key); + } + + accounts.save(&accounts_path).await?; + + println!("Success! Account added."); + }, + AccountCommand::List => { + let iter = accounts.iter_accounts(); + if iter.len() == 0 { + eprintln!("There are no accounts."); + return Ok(ExitCode::FAILURE); + } + + let sel_id = accounts.get_selected_account().map(|(id, _)| id); + let mut first = true; + for (id, account) in iter { + if !first { + println!(); + } + + first = false; + display_account(account, sel_id.is_some_and(|s| s == id), false); + } }, - AccountCommand::SignIn => { + AccountCommand::Info => { + let Some((_, account)) = accounts.get_selected_account() else { + eprintln!("No account selected."); + return Ok(ExitCode::FAILURE); + }; + display_account(account, false, true); + }, + AccountCommand::Refresh => { + let Some(account) = accounts.get_selected_account_mut() else { + eprintln!("No account selected."); + return Ok(ExitCode::FAILURE); + }; + + let client = MsaAccount::create_client(); + + match account { + Account::MSA(msa_acct) => { + msa_acct.log_in_silent(&client).await?; + println!("Successfully refreshed account: {}", account); + }, + _ => { + eprintln!("Cannot refresh non-MSA account."); + return Ok(ExitCode::FAILURE); + } + } + + accounts.save(&accounts_path).await?; } } } -- cgit v1.2.3-70-g09d2