Skip to main content

steam_user/services/
phone.rs

1//! Phone number management services.
2
3use scraper::{Html, Selector};
4use serde_json::Value;
5
6use crate::{
7    client::SteamUser,
8    endpoint::steam_endpoint,
9    error::SteamUserError,
10    types::{AddPhoneNumberResponse, ConfirmPhoneCodeResponse, RemovePhoneResult},
11};
12
13impl SteamUser {
14    /// Retrieves the current phone number status associated with the
15    /// authenticated Steam account.
16    ///
17    /// Scrapes the phone management page at `https://store.steampowered.com/phone/manage`.
18    ///
19    /// # Returns
20    ///
21    /// Returns:
22    /// - `Ok(Some("none"))` if no phone number is linked.
23    /// - `Ok(Some("Ends in XX"))` describing the phone if one is bound.
24    /// - `Ok(None)` if the status cannot be determined.
25    #[steam_endpoint(GET, host = Store, path = "/phone/manage", kind = Read)]
26    pub async fn get_phone_number_status(&self) -> Result<Option<String>, SteamUserError> {
27        let response = self.get_path("/phone/manage").send().await?.text().await?;
28
29        let document = Html::parse_document(&response);
30        let header_selector = Selector::parse("h2.pageheader").expect("valid CSS selector");
31
32        if let Some(header) = document.select(&header_selector).next() {
33            let header_text = header.text().collect::<String>().trim().to_string();
34            if header_text == "Add a phone number to your account" {
35                return Ok(Some("none".to_string()));
36            } else if header_text == "Manage phone number" {
37                let desc_selector = Selector::parse(".phone_header_description > span").expect("valid CSS selector");
38                if let Some(desc) = document.select(&desc_selector).next() {
39                    return Ok(Some(desc.text().collect::<String>().trim().to_string()));
40                }
41            }
42        }
43
44        Ok(None)
45    }
46
47    /// Initiates the process of adding a phone number to the user's Steam
48    /// account.
49    ///
50    /// # Arguments
51    ///
52    /// * `phone` - The phone number to add (including country code, e.g., "+1
53    ///   555-123-4567").
54    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
55    pub async fn add_phone_number(&self, phone: &str) -> Result<AddPhoneNumberResponse, SteamUserError> {
56        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?;
57
58        Ok(response)
59    }
60
61    /// Confirms the SMS verification code received after calling
62    /// [`Self::add_phone_number`].
63    ///
64    /// # Arguments
65    ///
66    /// * `code` - The numeric code received via SMS.
67    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
68    pub async fn confirm_phone_code_for_add(&self, code: &str) -> Result<ConfirmPhoneCodeResponse, SteamUserError> {
69        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?;
70
71        Ok(response)
72    }
73
74    /// Initiates a resend of the SMS verification code or retries email
75    /// verification.
76    #[steam_endpoint(POST, host = Store, path = "/phone/add_ajaxop", kind = Write)]
77    pub async fn resend_phone_verification_code(&self) -> Result<Value, SteamUserError> {
78        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?;
79
80        Ok(response)
81    }
82
83    /// Determines the available methods for removing the phone number from the
84    /// account.
85    ///
86    /// Scrapes the Steam Help wizard to find whether removal can be done via
87    /// SMS, Email, or Mobile App.
88    #[steam_endpoint(GET, host = Help, path = "/en/wizard/HelpRemovePhoneNumber", kind = Recovery)]
89    pub async fn get_remove_phone_number_type(&self) -> Result<Option<RemovePhoneResult>, SteamUserError> {
90        let response = self.get_path("/en/wizard/HelpRemovePhoneNumber?redir=store/account").send().await?.text().await?;
91
92        let document = Html::parse_document(&response);
93        let button_selector = Selector::parse("a.help_wizard_button").expect("valid CSS selector");
94
95        for button in document.select(&button_selector) {
96            let text = button.text().collect::<String>().trim().to_string();
97            let link = button.value().attr("href").map(|l| if l.starts_with("http") { l.to_string() } else { format!("https://help.steampowered.com{}", l) });
98
99            let mut wizard_param = None;
100            if let Some(ref l) = link {
101                if let Ok(url) = url::Url::parse(l) {
102                    let map: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
103                    if !map.is_empty() {
104                        wizard_param = serde_json::to_value(map).ok();
105                    }
106                }
107            }
108
109            if text == "Send a confirmation to my Steam Mobile app" {
110                return Ok(Some(RemovePhoneResult { success: true, method: Some(8), confirm_type: Some("SteamAppConfirm".into()), link, wizard_param }));
111            }
112
113            if text.starts_with("Text an account verification code to my phone number ending in") {
114                return Ok(Some(RemovePhoneResult { success: true, method: Some(4), confirm_type: Some("PhoneTextingConfirm".into()), link, wizard_param }));
115            }
116
117            if text.starts_with("Email an account verification code to") {
118                return Ok(Some(RemovePhoneResult { success: true, method: Some(2), confirm_type: Some("EmailConfirm".into()), link, wizard_param }));
119            }
120        }
121
122        Ok(None)
123    }
124
125    /// Sends an account recovery code via the specified method.
126    /// Methods: 2 = Email, 4 = SMS, 8 = Mobile App.
127    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendAccountRecoveryCode", kind = Recovery)]
128    pub async fn send_account_recovery_code(&self, wizard_param: Value, method: i32) -> Result<Value, SteamUserError> {
129        let mut params = wizard_param;
130        if let Some(obj) = params.as_object_mut() {
131            obj.insert("method".to_string(), Value::Number(method.into()));
132            obj.insert("n".to_string(), Value::Number(1.into()));
133            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
134            obj.insert("gamepad".to_string(), Value::String("0".into()));
135        }
136
137        let response: Value = self.post_path("/en/wizard/AjaxSendAccountRecoveryCode").form(&params).send().await?.json().await?;
138
139        Ok(response)
140    }
141
142    /// Confirm the code when removing a phone number.
143    #[steam_endpoint(GET, host = Help, path = "/en/wizard/AjaxVerifyAccountRecoveryCode", kind = Recovery)]
144    pub async fn confirm_remove_phone_number_code(&self, wizard_param: Value, code: &str) -> Result<Value, SteamUserError> {
145        let mut params = wizard_param;
146        if let Some(obj) = params.as_object_mut() {
147            obj.insert("code".to_string(), Value::String(code.to_string()));
148            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
149            obj.insert("gamepad".to_string(), Value::String("0".into()));
150        }
151
152        let response: Value = self.get_path("/en/wizard/AjaxVerifyAccountRecoveryCode").query(&params).send().await?.json().await?;
153
154        Ok(response)
155    }
156
157    /// Sends a confirmation request to the Steam Mobile app.
158    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendConfirmation2SteamMobileApp", kind = Recovery)]
159    pub async fn send_confirmation_2_steam_mobile_app(&self, wizard_param: Value) -> Result<Value, SteamUserError> {
160        let mut params = wizard_param;
161        if let Some(obj) = params.as_object_mut() {
162            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
163            obj.insert("gamepad".to_string(), Value::String("0".into()));
164        }
165
166        let response: Value = self.post_path("/en/wizard/AjaxSendConfirmation2SteamMobileApp").form(&params).send().await?.json().await?;
167
168        Ok(response)
169    }
170
171    /// Sends the final confirmation request to the Steam Mobile app.
172    #[steam_endpoint(POST, host = Help, path = "/en/wizard/AjaxSendConfirmation2SteamMobileAppFinal", kind = Recovery)]
173    pub async fn send_confirmation_2_steam_mobile_app_final(&self, wizard_param: Value) -> Result<Value, SteamUserError> {
174        let mut params = wizard_param;
175        if let Some(obj) = params.as_object_mut() {
176            obj.insert("wizard_ajax".to_string(), Value::String("1".into()));
177            obj.insert("gamepad".to_string(), Value::String("0".into()));
178        }
179
180        let response: Value = self.post_path("/en/wizard/AjaxSendConfirmation2SteamMobileAppFinal").form(&params).send().await?.json().await?;
181
182        Ok(response)
183    }
184}