steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Phone number management services.

use scraper::{Html, Selector};
use serde_json::Value;

use crate::{
    client::SteamUser,
    endpoint::steam_endpoint,
    error::SteamUserError,
    types::{AddPhoneNumberResponse, ConfirmPhoneCodeResponse, RemovePhoneResult},
};

impl SteamUser {
    /// Retrieves the current phone number status associated with the
    /// authenticated Steam account.
    ///
    /// Scrapes the phone management page at `https://store.steampowered.com/phone/manage`.
    ///
    /// # Returns
    ///
    /// Returns:
    /// - `Ok(Some("none"))` if no phone number is linked.
    /// - `Ok(Some("Ends in XX"))` describing the phone if one is bound.
    /// - `Ok(None)` if the status cannot be determined.
    #[steam_endpoint(GET, host = Store, path = "/phone/manage", kind = Read)]
    pub async fn get_phone_number_status(&self) -> Result<Option<String>, SteamUserError> {
        let response = self.get_path("/phone/manage").send().await?.text().await?;

        let document = Html::parse_document(&response);
        let header_selector = Selector::parse("h2.pageheader").expect("valid CSS selector");

        if let Some(header) = document.select(&header_selector).next() {
            let header_text = header.text().collect::<String>().trim().to_string();
            if header_text == "Add a phone number to your account" {
                return Ok(Some("none".to_string()));
            } else if header_text == "Manage phone number" {
                let desc_selector = Selector::parse(".phone_header_description > span").expect("valid CSS selector");
                if let Some(desc) = document.select(&desc_selector).next() {
                    return Ok(Some(desc.text().collect::<String>().trim().to_string()));
                }
            }
        }

        Ok(None)
    }

    /// Initiates the process of adding a phone number to the user's Steam
    /// account.
    ///
    /// # Arguments
    ///
    /// * `phone` - The phone number to add (including country code, e.g., "+1
    ///   555-123-4567").
    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
    pub async fn add_phone_number(&self, phone: &str) -> Result<AddPhoneNumberResponse, SteamUserError> {
        let response: AddPhoneNumberResponse = self.post_path("/phone/add_ajaxop").header("referer", "https://store.steampowered.com/phone/add").form(&[("op", "get_phone_number"), ("input", phone), ("confirmed", "1"), ("checkfortos", "1"), ("bisediting", "0"), ("token", "0")]).send().await?.json().await?;

        Ok(response)
    }

    /// Confirms the SMS verification code received after calling
    /// [`Self::add_phone_number`].
    ///
    /// # Arguments
    ///
    /// * `code` - The numeric code received via SMS.
    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
    pub async fn confirm_phone_code_for_add(&self, code: &str) -> Result<ConfirmPhoneCodeResponse, SteamUserError> {
        let response: ConfirmPhoneCodeResponse = self.post_path("/phone/add_ajaxop").form(&[("op", "get_sms_code"), ("input", code), ("confirmed", "1"), ("checkfortos", "1"), ("bisediting", "0"), ("token", "0")]).send().await?.json().await?;

        Ok(response)
    }

    /// Initiates a resend of the SMS verification code or retries email
    /// verification.
    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
    pub async fn resend_phone_verification_code(&self) -> Result<Value, SteamUserError> {
        let response: Value = self.post_path("/phone/add_ajaxop").form(&[("op", "retry_email_verification"), ("input", ""), ("confirmed", "1"), ("checkfortos", "1"), ("bisediting", "0"), ("token", "0")]).send().await?.json().await?;

        Ok(response)
    }

    /// Determines the available methods for removing the phone number from the
    /// account.
    ///
    /// Scrapes the Steam Help wizard to find whether removal can be done via
    /// SMS, Email, or Mobile App.
    #[steam_endpoint(GET, host = Help, path = "/en/wizard/HelpRemovePhoneNumber", kind = Recovery)]
    pub async fn get_remove_phone_number_type(&self) -> Result<Option<RemovePhoneResult>, SteamUserError> {
        let response = self.get_path("/en/wizard/HelpRemovePhoneNumber?redir=store/account").send().await?.text().await?;

        let document = Html::parse_document(&response);
        let button_selector = Selector::parse("a.help_wizard_button").expect("valid CSS selector");

        for button in document.select(&button_selector) {
            let text = button.text().collect::<String>().trim().to_string();
            let link = button.value().attr("href").map(|l| if l.starts_with("http") { l.to_string() } else { format!("https://help.steampowered.com{}", l) });

            let mut wizard_param = None;
            if let Some(ref l) = link {
                if let Ok(url) = url::Url::parse(l) {
                    let map: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
                    if !map.is_empty() {
                        wizard_param = serde_json::to_value(map).ok();
                    }
                }
            }

            if text == "Send a confirmation to my Steam Mobile app" {
                return Ok(Some(RemovePhoneResult { success: true, method: Some(8), confirm_type: Some("SteamAppConfirm".into()), link, wizard_param }));
            }

            if text.starts_with("Text an account verification code to my phone number ending in") {
                return Ok(Some(RemovePhoneResult { success: true, method: Some(4), confirm_type: Some("PhoneTextingConfirm".into()), link, wizard_param }));
            }

            if text.starts_with("Email an account verification code to") {
                return Ok(Some(RemovePhoneResult { success: true, method: Some(2), confirm_type: Some("EmailConfirm".into()), link, wizard_param }));
            }
        }

        Ok(None)
    }

    /// Sends an account recovery code via the specified method.
    /// Methods: 2 = Email, 4 = SMS, 8 = Mobile App.
    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendAccountRecoveryCode", kind = Recovery)]
    pub async fn send_account_recovery_code(&self, wizard_param: Value, method: i32) -> Result<Value, SteamUserError> {
        let mut params = wizard_param;
        if let Some(obj) = params.as_object_mut() {
            obj.insert("method".to_string(), Value::Number(method.into()));
            obj.insert("n".to_string(), Value::Number(1.into()));
            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
            obj.insert("gamepad".to_string(), Value::String("0".into()));
        }

        let response: Value = self.post_path("/en/wizard/AjaxSendAccountRecoveryCode").form(&params).send().await?.json().await?;

        Ok(response)
    }

    /// Confirm the code when removing a phone number.
    #[steam_endpoint(GET, host = Help, path = "/en/wizard/AjaxVerifyAccountRecoveryCode", kind = Recovery)]
    pub async fn confirm_remove_phone_number_code(&self, wizard_param: Value, code: &str) -> Result<Value, SteamUserError> {
        let mut params = wizard_param;
        if let Some(obj) = params.as_object_mut() {
            obj.insert("code".to_string(), Value::String(code.to_string()));
            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
            obj.insert("gamepad".to_string(), Value::String("0".into()));
        }

        let response: Value = self.get_path("/en/wizard/AjaxVerifyAccountRecoveryCode").query(&params).send().await?.json().await?;

        Ok(response)
    }

    /// Sends a confirmation request to the Steam Mobile app.
    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendConfirmation2SteamMobileApp", kind = Recovery)]
    pub async fn send_confirmation_2_steam_mobile_app(&self, wizard_param: Value) -> Result<Value, SteamUserError> {
        let mut params = wizard_param;
        if let Some(obj) = params.as_object_mut() {
            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
            obj.insert("gamepad".to_string(), Value::String("0".into()));
        }

        let response: Value = self.post_path("/en/wizard/AjaxSendConfirmation2SteamMobileApp").form(&params).send().await?.json().await?;

        Ok(response)
    }

    /// Sends the final confirmation request to the Steam Mobile app.
    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendConfirmation2SteamMobileAppFinal", kind = Recovery)]
    pub async fn send_confirmation_2_steam_mobile_app_final(&self, wizard_param: Value) -> Result<Value, SteamUserError> {
        let mut params = wizard_param;
        if let Some(obj) = params.as_object_mut() {
            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
            obj.insert("gamepad".to_string(), Value::String("0".into()));
        }

        let response: Value = self.post_path("/en/wizard/AjaxSendConfirmation2SteamMobileAppFinal").form(&params).send().await?.json().await?;

        Ok(response)
    }
}