diff --git a/Cargo.lock b/Cargo.lock index 55da01e..4d40ff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -92,6 +92,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -848,6 +857,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -896,6 +914,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "lock_api" version = "0.4.7" @@ -915,6 +939,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "mac" version = "0.1.1" @@ -969,6 +1002,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.5.3" @@ -1002,6 +1041,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint-dig" version = "0.7.0" @@ -1084,6 +1133,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +[[package]] +name = "oncemutex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -1213,6 +1268,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phonenumber" +version = "0.3.2+8.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f" +dependencies = [ + "bincode", + "either", + "fnv", + "itertools", + "lazy_static 1.4.0", + "nom", + "quick-xml", + "regex", + "regex-cache", + "serde", + "serde_derive", + "thiserror", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1315,7 +1390,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.6.26", "rusty-fork", "tempfile", ] @@ -1422,6 +1497,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.18" @@ -1589,13 +1673,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-cache" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793" +dependencies = [ + "lru-cache", + "oncemutex", + "regex", + "regex-syntax 0.6.26", ] [[package]] @@ -1604,6 +1700,12 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -2087,6 +2189,7 @@ dependencies = [ "lazy_static 1.4.0", "log", "maplit", + "phonenumber", "protobuf", "protobuf-codegen", "protobuf-json-mapping", @@ -2121,6 +2224,7 @@ dependencies = [ "hmac-sha1", "lazy_static 1.4.0", "log", + "phonenumber", "proptest", "qrcode", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index d2b0bc8..e6ffb9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ secrecy = { version = "0.8", features = ["serde"] } zeroize = "^1.4.3" serde_path_to_error = "0.1.11" update-informer = { version = "1.0.0", optional = true, default-features = false, features = ["github"] } +phonenumber = "0.3" [dev-dependencies] tempdir = "0.3" diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 7a72483..6bdcbfb 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -1,7 +1,9 @@ use log::*; +use phonenumber::PhoneNumber; use secrecy::ExposeSecret; use steamguard::{ - accountlinker::AccountLinkSuccess, AccountLinkError, AccountLinker, FinalizeLinkError, + accountlinker::AccountLinkSuccess, phonelinker::PhoneLinker, steamapi::PhoneClient, + token::Tokens, transport::WebApiTransport, AccountLinkError, AccountLinker, FinalizeLinkError, }; use crate::{tui, AccountManager}; @@ -25,11 +27,11 @@ impl ManifestCommand for SetupCommand { ); } info!("Logging in to {}", username); - let session = + let tokens = crate::do_login_raw(username).expect("Failed to log in. Account has not been linked."); info!("Adding authenticator..."); - let mut linker = AccountLinker::new(session); + let mut linker = AccountLinker::new(tokens); let link: AccountLinkSuccess; loop { match linker.link() { @@ -37,18 +39,9 @@ impl ManifestCommand for SetupCommand { link = a; break; } - Err(AccountLinkError::MustRemovePhoneNumber) => { - println!("There is already a phone number on this account, please remove it and try again."); - bail!("There is already a phone number on this account, please remove it and try again."); - } Err(AccountLinkError::MustProvidePhoneNumber) => { - println!("Enter your phone number in the following format: +1 123-456-7890"); - print!("Phone number: "); - linker.phone_number = tui::prompt().replace(&['(', ')', '-'][..], ""); - } - Err(AccountLinkError::AuthenticatorPresent) => { - println!("An authenticator is already present on this account."); - bail!("An authenticator is already present on this account."); + eprintln!("Looks like you don't have a phone number on this account."); + do_add_phone_number(linker.tokens())?; } Err(AccountLinkError::MustConfirmEmail) => { println!("Check your email and click the link."); @@ -70,7 +63,7 @@ impl ManifestCommand for SetupCommand { Ok(_) => {} Err(err) => { error!("Aborting the account linking process because we failed to save the manifest. This is really bad. Here is the error: {}", err); - println!( + eprintln!( "Just in case, here is the account info. Save it somewhere just in case!\n{:#?}", manager.get_account(&account_name).unwrap().lock().unwrap() ); @@ -83,7 +76,7 @@ impl ManifestCommand for SetupCommand { .expect("account was not present in manifest"); let mut account = account_arc.lock().unwrap(); - println!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code.expose_secret()); + eprintln!("Authenticator has not yet been linked. Before continuing with finalization, please take the time to write down your revocation code: {}", account.revocation_code.expose_secret()); tui::pause(); debug!("attempting link finalization"); @@ -115,11 +108,11 @@ impl ManifestCommand for SetupCommand { let revocation_code = account.revocation_code.clone(); drop(account); // explicitly drop the lock so we don't hang on the mutex - println!("Authenticator finalized."); + info!("Authenticator finalized."); match manager.save() { Ok(_) => {} Err(err) => { - println!( + error!( "Failed to save manifest, but we were able to save it before. {}", err ); @@ -127,7 +120,7 @@ impl ManifestCommand for SetupCommand { } } - println!( + eprintln!( "Authenticator has been finalized. Please actually write down your revocation code: {}", revocation_code.expose_secret() ); @@ -135,3 +128,52 @@ impl ManifestCommand for SetupCommand { Ok(()) } } + +pub fn do_add_phone_number(tokens: &Tokens) -> anyhow::Result<()> { + let client = PhoneClient::new(WebApiTransport::new()); + + let linker = PhoneLinker::new(client, tokens.clone()); + + let phone_number: PhoneNumber; + loop { + eprintln!("Enter your phone number, including country code, in this format: +1 1234567890"); + eprint!("Phone number: "); + let number = tui::prompt(); + match phonenumber::parse(None, &number) { + Ok(p) => { + phone_number = p; + break; + } + Err(err) => { + error!("Failed to parse phone number: {}", err); + } + } + } + + let resp = linker.set_account_phone_number(phone_number)?; + + eprintln!( + "Please click the link in the email sent to {}", + resp.confirmation_email_address() + ); + tui::pause(); + + debug!("sending phone verification code"); + linker.send_phone_verification_code(0)?; + + loop { + eprint!("Enter the code sent to your phone: "); + let code = tui::prompt(); + + match linker.verify_account_phone_with_code(code) { + Ok(_) => break, + Err(err) => { + error!("Failed to verify phone number: {}", err); + } + } + } + + info!("Successfully added phone number to account"); + + Ok(()) +} diff --git a/steamguard/Cargo.toml b/steamguard/Cargo.toml index 23aef9e..c56001c 100644 --- a/steamguard/Cargo.toml +++ b/steamguard/Cargo.toml @@ -33,6 +33,7 @@ zeroize = "^1.4.3" protobuf = "3.2.0" protobuf-json-mapping = "3.2.0" hmac-sha256 = "1.1.7" +phonenumber = "0.3" [build-dependencies] anyhow = "^1.0" diff --git a/steamguard/protobufs/service_phone.proto b/steamguard/protobufs/service_phone.proto new file mode 100644 index 0000000..b086845 --- /dev/null +++ b/steamguard/protobufs/service_phone.proto @@ -0,0 +1,50 @@ + +message CPhone_AddPhoneToAccount_Response { + optional bool success = 1; + optional int32 phone_number_type = 2; +} + +message CPhone_ConfirmAddPhoneToAccount_Request { + optional fixed64 steamid = 1; + optional string stoken = 2; +} + +message CPhone_IsAccountWaitingForEmailConfirmation_Request { +} + +message CPhone_IsAccountWaitingForEmailConfirmation_Response { + optional bool awaiting_email_confirmation = 1; + optional uint32 seconds_to_wait = 2; +} + +message CPhone_SendPhoneVerificationCode_Request { + optional uint32 language = 1; +} + +message CPhone_SendPhoneVerificationCode_Response { +} + +message CPhone_SetAccountPhoneNumber_Request { + optional string phone_number = 1; + optional string phone_country_code = 2; +} + +message CPhone_SetAccountPhoneNumber_Response { + optional string confirmation_email_address = 1; + optional string phone_number_formatted = 2; +} + +message CPhone_VerifyAccountPhoneWithCode_Request { + optional string code = 1; +} + +message CPhone_VerifyAccountPhoneWithCode_Response { +} + +service Phone { + rpc ConfirmAddPhoneToAccount (.CPhone_ConfirmAddPhoneToAccount_Request) returns (.CPhone_AddPhoneToAccount_Response); + rpc IsAccountWaitingForEmailConfirmation (.CPhone_IsAccountWaitingForEmailConfirmation_Request) returns (.CPhone_IsAccountWaitingForEmailConfirmation_Response); + rpc SendPhoneVerificationCode (.CPhone_SendPhoneVerificationCode_Request) returns (.CPhone_SendPhoneVerificationCode_Response); + rpc SetAccountPhoneNumber (.CPhone_SetAccountPhoneNumber_Request) returns (.CPhone_SetAccountPhoneNumber_Response); + rpc VerifyAccountPhoneWithCode (.CPhone_VerifyAccountPhoneWithCode_Request) returns (.CPhone_VerifyAccountPhoneWithCode_Response); +} diff --git a/steamguard/src/accountlinker.rs b/steamguard/src/accountlinker.rs index 9e18988..b4e381b 100644 --- a/steamguard/src/accountlinker.rs +++ b/steamguard/src/accountlinker.rs @@ -11,7 +11,6 @@ use thiserror::Error; #[derive(Debug)] pub struct AccountLinker { device_id: String, - pub phone_number: String, pub account: Option, pub finalized: bool, tokens: Tokens, @@ -22,7 +21,6 @@ impl AccountLinker { pub fn new(tokens: Tokens) -> AccountLinker { Self { device_id: generate_device_id(), - phone_number: "".into(), account: None, finalized: false, tokens, @@ -30,29 +28,11 @@ impl AccountLinker { } } + pub fn tokens(&self) -> &Tokens { + &self.tokens + } + pub fn link(&mut self) -> anyhow::Result { - // let has_phone = self.client.has_phone()?; - - // if has_phone && !self.phone_number.is_empty() { - // return Err(AccountLinkError::MustRemovePhoneNumber); - // } - // if !has_phone && self.phone_number.is_empty() { - // return Err(AccountLinkError::MustProvidePhoneNumber); - // } - - // if !has_phone { - // if self.sent_confirmation_email { - // if !self.client.check_email_confirmation()? { - // return Err(anyhow!("Failed email confirmation check"))?; - // } - // } else if !self.client.add_phone_number(self.phone_number.clone())? { - // return Err(anyhow!("Failed to add phone number"))?; - // } else { - // self.sent_confirmation_email = true; - // return Err(AccountLinkError::MustConfirmEmail); - // } - // } - let access_token = self.tokens.access_token(); let steam_id = access_token.decode()?.steam_id(); @@ -162,14 +142,13 @@ pub enum AccountLinkError { /// No phone number on the account #[error("A phone number is needed, but not already present on the account.")] MustProvidePhoneNumber, - /// A phone number is already on the account - #[error("A phone number was provided, but one is already present on the account.")] - MustRemovePhoneNumber, /// User need to click link from confirmation email #[error("An email has been sent to the user's email, click the link in that email.")] MustConfirmEmail, - #[error("Authenticator is already present.")] + #[error("Authenticator is already present on this account.")] AuthenticatorPresent, + #[error("You are sending too many requests to Steam, and we got rate limited. Wait at least a couple hours and try again.")] + RateLimitExceeded, #[error("Steam was unable to link the authenticator to the account. No additional information about this error is available. This is a Steam error, not a steamguard-cli error. Try adding a phone number to your Steam account (which you can do here: https://store.steampowered.com/phone/add), or try again later.")] GenericFailure, #[error("Steam returned an unexpected error code: {0:?}")] @@ -181,10 +160,13 @@ pub enum AccountLinkError { impl From for AccountLinkError { fn from(result: EResult) -> Self { match result { + EResult::RateLimitExceeded => AccountLinkError::RateLimitExceeded, + EResult::NoVerifiedPhone => AccountLinkError::MustProvidePhoneNumber, EResult::DuplicateRequest => AccountLinkError::AuthenticatorPresent, // If the user has no phone number on their account, it will always return this status code. // However, this does not mean that this status just means "no phone number". It can also // be literally anything else, so that's why we return GenericFailure here. + // update 2023: This may be no longer true, now it seems to return NoVerifiedPhone if there is no phone number. We'll see. EResult::Fail => AccountLinkError::GenericFailure, r => AccountLinkError::UnknownEResult(r), } diff --git a/steamguard/src/lib.rs b/steamguard/src/lib.rs index 148e3a3..cafde52 100644 --- a/steamguard/src/lib.rs +++ b/steamguard/src/lib.rs @@ -30,6 +30,7 @@ extern crate maplit; pub mod accountlinker; mod api_responses; mod confirmation; +pub mod phonelinker; pub mod protobufs; mod qrapprover; pub mod refresher; diff --git a/steamguard/src/phonelinker.rs b/steamguard/src/phonelinker.rs new file mode 100644 index 0000000..02115ce --- /dev/null +++ b/steamguard/src/phonelinker.rs @@ -0,0 +1,166 @@ +use crate::protobufs::service_phone::*; +use crate::transport::TransportError; +use crate::{ + steamapi::{EResult, PhoneClient}, + token::Tokens, + transport::WebApiTransport, +}; + +pub use phonenumber::PhoneNumber; + +pub struct PhoneLinker { + client: PhoneClient, + tokens: Tokens, +} + +impl PhoneLinker { + pub fn new(client: PhoneClient, tokens: Tokens) -> Self { + Self { client, tokens } + } + + /// If successful, wait for the user to click the link in the email, then immediately call [`send_phone_verification_code`]. + pub fn set_account_phone_number( + &self, + phone_number: PhoneNumber, + ) -> Result { + // This results in an email being sent to the account's email address with a link to click on to confirm the phone number. + // This endpoint also does almost no validation of the phone number. It only validates it after the user clicks the link. + + // `phone_number` needs to include the country code in the format `11234567890` + + let mut req = CPhone_SetAccountPhoneNumber_Request::new(); + req.set_phone_number( + phone_number + .format() + .mode(phonenumber::Mode::E164) + .to_string(), + ); + req.set_phone_country_code(phone_number.code().value().to_string()); + + let resp = self + .client + .set_account_phone_number(req, self.tokens.access_token())?; + + if resp.result != EResult::Pending { + return Err(SetPhoneNumberError::UnknownEResult(resp.result)); + } + + let resp = resp.into_response_data(); + Ok(resp.into()) + } + + // confirm_add_phone_to_account is actually not needed, because it's performed by the user clicking the link in the email. + + /// language 0 is english + pub fn send_phone_verification_code(&self, language: u32) -> anyhow::Result<()> { + let mut req = CPhone_SendPhoneVerificationCode_Request::new(); + req.set_language(language); + + let resp = self + .client + .send_phone_verification_code(req, self.tokens.access_token())?; + + if resp.result != EResult::OK { + return Err(anyhow::anyhow!( + "Failed to send phone verification code: {:?}", + resp.result + )); + } + + Ok(()) + } + + pub fn verify_account_phone_with_code( + &self, + code: String, + ) -> anyhow::Result<(), VerifyPhoneError> { + let mut req = CPhone_VerifyAccountPhoneWithCode_Request::new(); + req.set_code(code); + + let resp = self + .client + .verify_account_phone_with_code(req, self.tokens.access_token())?; + + if resp.result != EResult::OK { + return Err(resp.result.into()); + } + + Ok(()) + } + + /// If true, returns `Some` with the value inside being the time in seconds until the email expires. + pub fn is_account_waiting_for_email_confirmation(&self) -> anyhow::Result> { + let req = CPhone_IsAccountWaitingForEmailConfirmation_Request::new(); + + let resp = self + .client + .is_account_waiting_for_email_confirmation(req, self.tokens.access_token())?; + + if resp.result != EResult::OK { + return Err(anyhow::anyhow!( + "Failed to check if account is waiting for email confirmation: {:?}", + resp.result + )); + } + + let resp = resp.into_response_data(); + if !resp.awaiting_email_confirmation() { + return Ok(None); + } + + Ok(resp.seconds_to_wait) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SetPhoneNumberError { + #[error("Transport error: {0}")] + TransportError(#[from] TransportError), + #[error("Steam says: {0:?}")] + UnknownEResult(EResult), +} + +impl From for SetPhoneNumberError { + fn from(result: EResult) -> Self { + SetPhoneNumberError::UnknownEResult(result) + } +} + +#[derive(Debug)] +pub struct SetAccountPhoneNumberResponse { + confirmation_email_address: String, + phone_number_formatted: String, +} + +impl SetAccountPhoneNumberResponse { + pub fn confirmation_email_address(&self) -> &str { + &self.confirmation_email_address + } + + pub fn phone_number_formatted(&self) -> &str { + &self.phone_number_formatted + } +} + +impl From for SetAccountPhoneNumberResponse { + fn from(mut resp: CPhone_SetAccountPhoneNumber_Response) -> Self { + Self { + confirmation_email_address: resp.take_confirmation_email_address(), + phone_number_formatted: resp.take_phone_number_formatted(), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum VerifyPhoneError { + #[error("Transport error: {0}")] + TransportError(#[from] TransportError), + #[error("Steam says: {0:?}")] + UnknownEResult(EResult), +} + +impl From for VerifyPhoneError { + fn from(result: EResult) -> Self { + VerifyPhoneError::UnknownEResult(result) + } +} diff --git a/steamguard/src/steamapi.rs b/steamguard/src/steamapi.rs index 8f24817..cea585f 100644 --- a/steamguard/src/steamapi.rs +++ b/steamguard/src/steamapi.rs @@ -1,4 +1,5 @@ pub mod authentication; +pub mod phone; pub mod twofactor; use crate::{ @@ -8,6 +9,7 @@ use reqwest::Url; use serde::Deserialize; pub use self::authentication::AuthenticationClient; +pub use self::phone::PhoneClient; pub use self::twofactor::TwoFactorClient; lazy_static! { diff --git a/steamguard/src/steamapi/phone.rs b/steamguard/src/steamapi/phone.rs new file mode 100644 index 0000000..fe04f23 --- /dev/null +++ b/steamguard/src/steamapi/phone.rs @@ -0,0 +1,128 @@ +use crate::{ + protobufs::service_phone::*, + token::Jwt, + transport::{Transport, TransportError}, +}; + +const SERVICE_NAME: &str = "IPhoneService"; + +use super::{ApiRequest, ApiResponse, BuildableRequest}; + +/// A client for the IPhoneService API. +#[derive(Debug)] +pub struct PhoneClient +where + T: Transport, +{ + transport: T, +} + +impl PhoneClient +where + T: Transport, +{ + #[must_use] + pub fn new(transport: T) -> Self { + Self { transport } + } + + pub fn set_account_phone_number( + &self, + req: CPhone_SetAccountPhoneNumber_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new(SERVICE_NAME, "SetAccountPhoneNumber", 1u32, req) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } + + pub fn send_phone_verification_code( + &self, + req: CPhone_SendPhoneVerificationCode_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new(SERVICE_NAME, "SendPhoneVerificationCode", 1u32, req) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } + + pub fn is_account_waiting_for_email_confirmation( + &self, + req: CPhone_IsAccountWaitingForEmailConfirmation_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new( + SERVICE_NAME, + "IsAccountWaitingForEmailConfirmation", + 1u32, + req, + ) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } + + pub fn confirm_add_phone_to_account( + &self, + req: CPhone_ConfirmAddPhoneToAccount_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new(SERVICE_NAME, "ConfirmAddPhoneToAccount", 1u32, req) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } + + pub fn verify_account_phone_with_code( + &self, + req: CPhone_VerifyAccountPhoneWithCode_Request, + access_token: &Jwt, + ) -> Result, TransportError> { + let req = ApiRequest::new(SERVICE_NAME, "VerifyAccountPhoneWithCode", 1u32, req) + .with_access_token(access_token); + let resp = self + .transport + .send_request::( + req, + )?; + Ok(resp) + } +} + +macro_rules! impl_buildable_req { + ($type:ty, $needs_auth:literal) => { + impl BuildableRequest for $type { + fn method() -> reqwest::Method { + reqwest::Method::POST + } + + fn requires_access_token() -> bool { + $needs_auth + } + } + }; +} + +impl_buildable_req!(CPhone_SetAccountPhoneNumber_Request, true); +impl_buildable_req!(CPhone_SendPhoneVerificationCode_Request, true); +impl_buildable_req!(CPhone_IsAccountWaitingForEmailConfirmation_Request, true); +impl_buildable_req!(CPhone_ConfirmAddPhoneToAccount_Request, true); +impl_buildable_req!(CPhone_VerifyAccountPhoneWithCode_Request, true);