2022-12-06 16:02:07 +01:00
use crate ::api_responses ::* ;
2021-08-01 14:43:18 +02:00
use log ::* ;
use reqwest ::{
2021-08-08 18:54:46 +02:00
blocking ::RequestBuilder ,
cookie ::CookieStore ,
header ::COOKIE ,
header ::{ HeaderMap , HeaderName , HeaderValue , SET_COOKIE } ,
Url ,
2021-08-01 14:43:18 +02:00
} ;
2022-06-19 20:44:18 +02:00
use secrecy ::{ CloneableSecret , DebugSecret , ExposeSecret , SerializableSecret } ;
2022-12-06 16:02:07 +01:00
use serde ::{ Deserialize , Serialize } ;
2021-08-08 19:37:18 +02:00
use serde_json ::Value ;
2021-08-08 00:47:39 +02:00
use std ::iter ::FromIterator ;
use std ::str ::FromStr ;
2021-03-24 22:49:09 +01:00
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
2022-06-19 20:44:18 +02:00
use zeroize ::Zeroize ;
2021-03-24 22:49:09 +01:00
2021-08-08 17:52:12 +02:00
lazy_static! {
2021-08-08 18:54:46 +02:00
static ref STEAM_COOKIE_URL : Url = " https://steamcommunity.com " . parse ::< Url > ( ) . unwrap ( ) ;
2021-08-08 21:25:27 +02:00
static ref STEAM_API_BASE : String = " https://api.steampowered.com " . into ( ) ;
2021-08-08 17:52:12 +02:00
}
2022-06-19 20:42:07 +02:00
#[ derive(Debug, Clone, Serialize, Deserialize, Zeroize) ]
#[ zeroize(drop) ]
2021-03-25 18:52:55 +01:00
pub struct Session {
2021-08-08 18:54:46 +02:00
#[ serde(rename = " SessionID " ) ]
pub session_id : String ,
#[ serde(rename = " SteamLogin " ) ]
pub steam_login : String ,
#[ serde(rename = " SteamLoginSecure " ) ]
pub steam_login_secure : String ,
2021-08-10 01:49:53 +02:00
#[ serde(default, rename = " WebCookie " ) ]
2022-01-15 01:21:36 +01:00
pub web_cookie : Option < String > ,
2021-08-08 18:54:46 +02:00
#[ serde(rename = " OAuthToken " ) ]
pub token : String ,
#[ serde(rename = " SteamID " ) ]
pub steam_id : u64 ,
2021-03-25 18:52:55 +01:00
}
2021-03-24 22:49:09 +01:00
2022-06-19 20:42:07 +02:00
impl SerializableSecret for Session { }
impl CloneableSecret for Session { }
impl DebugSecret for Session { }
2022-06-21 02:05:00 +02:00
/// Queries Steam for the current time.
///
/// Endpoint: `/ITwoFactorService/QueryTime/v0001`
///
/// Example Response:
/// ```json
/// {
/// "response": {
/// "server_time": "1655768666",
/// "skew_tolerance_seconds": "60",
/// "large_time_jink": "86400",
/// "probe_frequency_seconds": 3600,
/// "adjusted_time_probe_frequency_seconds": 300,
/// "hint_probe_frequency_seconds": 60,
/// "sync_timeout": 60,
/// "try_again_seconds": 900,
/// "max_attempts": 3
/// }
/// }
/// ```
pub fn get_server_time ( ) -> anyhow ::Result < QueryTimeResponse > {
2021-08-08 18:54:46 +02:00
let client = reqwest ::blocking ::Client ::new ( ) ;
let resp = client
. post ( " https://api.steampowered.com/ITwoFactorService/QueryTime/v0001 " )
. body ( " steamid=0 " )
2022-06-21 02:05:00 +02:00
. send ( ) ? ;
let resp : SteamApiResponse < QueryTimeResponse > = resp . json ( ) ? ;
2021-08-08 18:54:46 +02:00
2022-06-21 02:05:00 +02:00
return Ok ( resp . response ) ;
2021-03-24 22:49:09 +01:00
}
2021-04-04 15:38:34 +02:00
2021-08-09 06:09:34 +02:00
/// Provides raw access to the Steam API. Handles cookies, some deserialization, etc. to make it easier. It covers `ITwoFactorService` from the Steam web API, and some mobile app specific api endpoints.
2021-08-08 00:47:39 +02:00
#[ derive(Debug) ]
2021-08-08 17:52:12 +02:00
pub struct SteamApiClient {
2021-08-08 18:54:46 +02:00
cookies : reqwest ::cookie ::Jar ,
client : reqwest ::blocking ::Client ,
2022-06-19 20:42:07 +02:00
pub session : Option < secrecy ::Secret < Session > > ,
2021-08-08 00:47:39 +02:00
}
impl SteamApiClient {
2022-06-19 20:42:07 +02:00
pub fn new ( session : Option < secrecy ::Secret < Session > > ) -> SteamApiClient {
2021-08-08 18:54:46 +02:00
SteamApiClient {
2021-08-08 00:47:39 +02:00
cookies : reqwest ::cookie ::Jar ::default ( ) ,
client : reqwest ::blocking ::ClientBuilder ::new ( )
. cookie_store ( true )
. user_agent ( " Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30 " )
. default_headers ( HeaderMap ::from_iter ( hashmap! {
HeaderName ::from_str ( " X-Requested-With " ) . expect ( " could not build default request headers " ) = > HeaderValue ::from_str ( " com.valvesoftware.android.steam.community " ) . expect ( " could not build default request headers " )
} . into_iter ( ) ) )
. build ( )
. unwrap ( ) ,
2021-08-10 02:05:23 +02:00
session : session ,
2021-08-08 00:47:39 +02:00
}
2021-08-08 18:54:46 +02:00
}
fn build_session ( & self , data : & OAuthData ) -> Session {
2021-08-10 02:05:23 +02:00
trace! ( " SteamApiClient::build_session " ) ;
2021-08-08 18:54:46 +02:00
return Session {
token : data . oauth_token . clone ( ) ,
steam_id : data . steamid . parse ( ) . unwrap ( ) ,
steam_login : format ! ( " {}%7C%7C{} " , data . steamid , data . wgtoken ) ,
steam_login_secure : format ! ( " {}%7C%7C{} " , data . steamid , data . wgtoken_secure ) ,
2021-08-10 23:17:41 +02:00
session_id : self
. extract_session_id ( )
. expect ( " failed to extract session id from cookies " ) ,
2022-01-15 01:21:36 +01:00
web_cookie : Some ( data . webcookie . clone ( ) ) ,
2021-08-08 18:54:46 +02:00
} ;
}
fn extract_session_id ( & self ) -> Option < String > {
let cookies = self . cookies . cookies ( & STEAM_COOKIE_URL ) . unwrap ( ) ;
let all_cookies = cookies . to_str ( ) . unwrap ( ) ;
for cookie in all_cookies
. split ( " ; " )
. map ( | s | cookie ::Cookie ::parse ( s ) . unwrap ( ) )
{
if cookie . name ( ) = = " sessionid " {
return Some ( cookie . value ( ) . into ( ) ) ;
}
}
return None ;
}
pub fn save_cookies_from_response ( & mut self , response : & reqwest ::blocking ::Response ) {
let set_cookie_iter = response . headers ( ) . get_all ( SET_COOKIE ) ;
for c in set_cookie_iter {
c . to_str ( )
. into_iter ( )
. for_each ( | cookie_str | self . cookies . add_cookie_str ( cookie_str , & STEAM_COOKIE_URL ) ) ;
}
}
2021-08-10 23:17:41 +02:00
pub fn request < U : reqwest ::IntoUrl + std ::fmt ::Display > (
& self ,
method : reqwest ::Method ,
url : U ,
) -> RequestBuilder {
2021-08-10 02:40:06 +02:00
trace! ( " making request: {} {} " , method , url ) ;
2021-08-08 18:54:46 +02:00
self . cookies
. add_cookie_str ( " mobileClientVersion=0 (2.1.3) " , & STEAM_COOKIE_URL ) ;
self . cookies
. add_cookie_str ( " mobileClient=android " , & STEAM_COOKIE_URL ) ;
self . cookies
. add_cookie_str ( " Steam_Language=english " , & STEAM_COOKIE_URL ) ;
2021-08-10 02:40:06 +02:00
if let Some ( session ) = & self . session {
2021-08-10 23:17:41 +02:00
self . cookies . add_cookie_str (
2022-06-19 20:42:07 +02:00
format! ( " sessionid= {} " , session . expose_secret ( ) . session_id ) . as_str ( ) ,
2021-08-10 23:17:41 +02:00
& STEAM_COOKIE_URL ,
) ;
2021-08-10 02:40:06 +02:00
}
2021-08-08 18:54:46 +02:00
self . client
. request ( method , url )
. header ( COOKIE , self . cookies . cookies ( & STEAM_COOKIE_URL ) . unwrap ( ) )
}
2021-08-10 02:40:06 +02:00
pub fn get < U : reqwest ::IntoUrl + std ::fmt ::Display > ( & self , url : U ) -> RequestBuilder {
2021-08-08 18:54:46 +02:00
self . request ( reqwest ::Method ::GET , url )
}
2021-08-10 02:40:06 +02:00
pub fn post < U : reqwest ::IntoUrl + std ::fmt ::Display > ( & self , url : U ) -> RequestBuilder {
2021-08-08 18:54:46 +02:00
self . request ( reqwest ::Method ::POST , url )
}
/// Updates the cookie jar with the session cookies by pinging steam servers.
pub fn update_session ( & mut self ) -> anyhow ::Result < ( ) > {
trace! ( " SteamApiClient::update_session " ) ;
let resp = self
2021-08-08 00:47:39 +02:00
. get ( " https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client " . parse ::< Url > ( ) . unwrap ( ) )
. send ( ) ? ;
2021-08-08 18:54:46 +02:00
self . save_cookies_from_response ( & resp ) ;
trace! ( " {:?} " , resp ) ;
trace! ( " cookies: {:?} " , self . cookies ) ;
Ok ( ( ) )
}
/// Endpoint: POST /login/dologin
pub fn login (
& mut self ,
username : String ,
encrypted_password : String ,
twofactor_code : String ,
email_code : String ,
captcha_gid : String ,
captcha_text : String ,
rsa_timestamp : String ,
) -> anyhow ::Result < LoginResponse > {
let params = hashmap! {
" donotcache " = > format! (
" {} " ,
SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. unwrap ( )
. as_secs ( )
* 1000
) ,
" username " = > username ,
" password " = > encrypted_password ,
" twofactorcode " = > twofactor_code ,
" emailauth " = > email_code ,
" captchagid " = > captcha_gid ,
" captcha_text " = > captcha_text ,
" rsatimestamp " = > rsa_timestamp ,
" remember_login " = > " true " . into ( ) ,
" oauth_client_id " = > " DE45CD61 " . into ( ) ,
" oauth_scope " = > " read_profile write_profile read_client write_client " . into ( ) ,
} ;
let resp = self
. post ( " https://steamcommunity.com/login/dologin " )
. form ( & params )
. send ( ) ? ;
2021-08-10 02:05:23 +02:00
self . save_cookies_from_response ( & resp ) ;
2021-08-08 18:54:46 +02:00
let text = resp . text ( ) ? ;
trace! ( " raw login response: {} " , text ) ;
let login_resp : LoginResponse = serde_json ::from_str ( text . as_str ( ) ) ? ;
if let Some ( oauth ) = & login_resp . oauth {
2022-06-19 20:42:07 +02:00
self . session = Some ( secrecy ::Secret ::new ( self . build_session ( & oauth ) ) ) ;
2021-08-08 18:54:46 +02:00
}
return Ok ( login_resp ) ;
}
/// A secondary step in the login flow. Does not seem to always be needed?
/// Endpoints: provided by `login()`
pub fn transfer_login ( & mut self , login_resp : LoginResponse ) -> anyhow ::Result < OAuthData > {
match ( login_resp . transfer_urls , login_resp . transfer_parameters ) {
( Some ( urls ) , Some ( params ) ) = > {
debug! ( " received transfer parameters, relaying data... " ) ;
for url in urls {
trace! ( " posting transfer to {} " , url ) ;
let resp = self . client . post ( url ) . json ( & params ) . send ( ) ? ;
self . save_cookies_from_response ( & resp ) ;
}
let oauth = OAuthData {
oauth_token : params . auth ,
steamid : params . steamid . parse ( ) . unwrap ( ) ,
wgtoken : params . token_secure . clone ( ) , // guessing
wgtoken_secure : params . token_secure ,
webcookie : params . webcookie ,
} ;
2022-06-19 20:42:07 +02:00
self . session = Some ( secrecy ::Secret ::new ( self . build_session ( & oauth ) ) ) ;
2021-08-08 18:54:46 +02:00
return Ok ( oauth ) ;
}
( None , None ) = > {
bail! ( " did not receive transfer_urls and transfer_parameters " ) ;
}
( _ , None ) = > {
bail! ( " did not receive transfer_parameters " ) ;
}
( None , _ ) = > {
bail! ( " did not receive transfer_urls " ) ;
}
}
}
2021-08-08 19:37:18 +02:00
2022-12-05 16:32:58 +01:00
/// Likely removed now
///
2021-08-10 03:41:20 +02:00
/// One of the endpoints that handles phone number things. Can check to see if phone is present on account, and maybe do some other stuff. It's not really super clear.
///
/// Host: steamcommunity.com
2021-08-08 19:37:18 +02:00
/// Endpoint: POST /steamguard/phoneajax
2021-08-10 02:40:06 +02:00
/// Requires `sessionid` cookie to be set.
2021-08-08 19:37:18 +02:00
fn phoneajax ( & self , op : & str , arg : & str ) -> anyhow ::Result < bool > {
let mut params = hashmap! {
" op " = > op ,
" arg " = > arg ,
2022-06-19 20:42:07 +02:00
" sessionid " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . session_id . as_str ( ) ,
2021-08-08 19:37:18 +02:00
} ;
if op = = " check_sms_code " {
params . insert ( " checkfortos " , " 0 " ) ;
params . insert ( " skipvoip " , " 1 " ) ;
}
let resp = self
. post ( " https://steamcommunity.com/steamguard/phoneajax " )
. form ( & params )
. send ( ) ? ;
2021-08-10 03:41:20 +02:00
trace! ( " phoneajax: status={} " , resp . status ( ) ) ;
2021-08-08 19:37:18 +02:00
let result : Value = resp . json ( ) ? ;
2021-08-10 02:11:15 +02:00
trace! ( " phoneajax: {:?} " , result ) ;
2021-08-08 19:37:18 +02:00
if result [ " has_phone " ] ! = Value ::Null {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - found has_phone field " , op ) ;
2021-08-08 19:37:18 +02:00
return result [ " has_phone " ]
. as_bool ( )
. ok_or ( anyhow! ( " failed to parse has_phone field into boolean " ) ) ;
} else if result [ " success " ] ! = Value ::Null {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - found success field " , op ) ;
2021-08-08 19:37:18 +02:00
return result [ " success " ]
. as_bool ( )
. ok_or ( anyhow! ( " failed to parse success field into boolean " ) ) ;
} else {
2021-08-10 02:11:15 +02:00
trace! ( " op: {} - did not find any expected field " , op ) ;
2021-08-08 19:37:18 +02:00
return Ok ( false ) ;
}
}
2021-08-11 02:28:20 +02:00
/// Works similar to phoneajax. Used in the process to add a phone number to a steam account.
/// Valid ops:
/// - get_phone_number => `input` is treated as a phone number to add to the account. Yes, this is somewhat counter intuitive.
/// - resend_sms
2021-08-11 02:54:01 +02:00
/// - get_sms_code => `input` is treated as a the SMS code that was texted to the phone number. Again, this is somewhat counter intuitive. After this succeeds, the phone number is added to the account.
/// - email_verification => If the account is protected with steam guard email, a verification link is sent. After the link in the email is clicked, send this op. After, an SMS code is sent to the phone number.
/// - retry_email_verification
2021-08-11 02:28:20 +02:00
///
/// Host: store.steampowered.com
/// Endpoint: /phone/add_ajaxop
fn phone_add_ajaxop ( & self , op : & str , input : & str ) -> anyhow ::Result < ( ) > {
trace! ( " phone_add_ajaxop: op={} input={} " , op , input ) ;
let params = hashmap! {
" op " = > op ,
" input " = > input ,
2022-06-19 20:42:07 +02:00
" sessionid " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . session_id . as_str ( ) ,
2021-08-11 02:28:20 +02:00
} ;
let resp = self
2021-08-11 02:54:01 +02:00
. post ( " https://store.steampowered.com/phone/add_ajaxop " )
2021-08-11 02:28:20 +02:00
. form ( & params )
. send ( ) ? ;
trace! ( " phone_add_ajaxop: http status={} " , resp . status ( ) ) ;
let text = resp . text ( ) ? ;
trace! ( " phone_add_ajaxop response: {} " , text ) ;
todo! ( ) ;
}
2021-08-08 19:37:18 +02:00
pub fn has_phone ( & self ) -> anyhow ::Result < bool > {
return self . phoneajax ( " has_phone " , " null " ) ;
}
pub fn check_sms_code ( & self , sms_code : String ) -> anyhow ::Result < bool > {
return self . phoneajax ( " check_sms_code " , sms_code . as_str ( ) ) ;
}
pub fn check_email_confirmation ( & self ) -> anyhow ::Result < bool > {
return self . phoneajax ( " email_confirmation " , " " ) ;
}
pub fn add_phone_number ( & self , phone_number : String ) -> anyhow ::Result < bool > {
2021-08-10 03:41:20 +02:00
// return self.phoneajax("add_phone_number", phone_number.as_str());
todo! ( ) ;
}
/// Provides lots of juicy information, like if the number is a VOIP number.
/// Host: store.steampowered.com
/// Endpoint: POST /phone/validate
2022-12-05 16:28:10 +01:00
/// Body format: form data
/// Example:
2022-12-05 17:18:59 +01:00
/// ```form
2022-12-05 16:28:10 +01:00
/// sessionID=FOO&phoneNumber=%2B1+1234567890
/// ```
2021-08-10 03:41:20 +02:00
/// Found on page: https://store.steampowered.com/phone/add
2022-12-05 16:28:10 +01:00
pub fn phone_validate ( & self , phone_number : & String ) -> anyhow ::Result < PhoneValidateResponse > {
2021-08-10 23:17:41 +02:00
let params = hashmap! {
2022-12-05 16:28:10 +01:00
" sessionID " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . session_id . as_str ( ) ,
" phoneNumber " = > phone_number . as_str ( ) ,
2021-08-10 03:41:20 +02:00
} ;
2022-12-05 16:28:10 +01:00
let resp = self
. client
. post ( " https://store.steampowered.com/phone/validate " )
. form ( & params )
. send ( ) ?
. json ::< PhoneValidateResponse > ( ) ? ;
return Ok ( resp ) ;
2021-08-08 19:37:18 +02:00
}
2021-08-08 21:25:27 +02:00
/// Starts the authenticator linking process.
/// This doesn't check any prereqisites to ensure the request will pass validation on Steam's side (eg. sms/email confirmations).
/// A valid `Session` is required for this request. Cookies are not needed for this request, but they are set anyway.
///
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/AddAuthenticator/v0001
2021-08-10 23:17:41 +02:00
pub fn add_authenticator (
& mut self ,
device_id : String ,
) -> anyhow ::Result < AddAuthenticatorResponse > {
2021-08-08 21:25:27 +02:00
ensure! ( matches! ( self . session , Some ( _ ) ) ) ;
let params = hashmap! {
2022-06-19 20:42:07 +02:00
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . token . clone ( ) ,
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . steam_id . to_string ( ) ,
2021-08-08 21:25:27 +02:00
" authenticator_type " = > " 1 " . into ( ) ,
" device_identifier " = > device_id ,
" sms_phone_id " = > " 1 " . into ( ) ,
} ;
2021-08-10 02:05:23 +02:00
let resp = self
2021-08-08 21:25:27 +02:00
. post ( format! (
" {}/ITwoFactorService/AddAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
2021-08-10 02:05:23 +02:00
. send ( ) ? ;
self . save_cookies_from_response ( & resp ) ;
let text = resp . text ( ) ? ;
trace! ( " raw add authenticator response: {} " , text ) ;
2021-08-08 21:25:27 +02:00
2021-08-09 01:09:15 +02:00
let resp : SteamApiResponse < AddAuthenticatorResponse > = serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-08 21:25:27 +02:00
2021-08-09 01:09:15 +02:00
Ok ( resp . response )
2021-08-08 21:25:27 +02:00
}
2021-08-09 00:32:50 +02:00
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/FinalizeAddAuthenticator/v0001
pub fn finalize_authenticator (
& self ,
sms_code : String ,
code_2fa : String ,
2022-06-21 02:05:00 +02:00
time_2fa : u64 ,
2021-08-09 00:32:50 +02:00
) -> anyhow ::Result < FinalizeAddAuthenticatorResponse > {
ensure! ( matches! ( self . session , Some ( _ ) ) ) ;
let params = hashmap! {
2022-06-19 20:42:07 +02:00
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . steam_id . to_string ( ) ,
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . token . clone ( ) ,
2021-08-09 00:32:50 +02:00
" activation_code " = > sms_code ,
" authenticator_code " = > code_2fa ,
" authenticator_time " = > time_2fa . to_string ( ) ,
} ;
2021-08-10 04:11:09 +02:00
let resp = self
2021-08-09 00:32:50 +02:00
. post ( format! (
" {}/ITwoFactorService/FinalizeAddAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
2021-08-10 04:11:09 +02:00
. send ( ) ? ;
let text = resp . text ( ) ? ;
trace! ( " raw finalize authenticator response: {} " , text ) ;
2021-08-10 23:17:41 +02:00
let resp : SteamApiResponse < FinalizeAddAuthenticatorResponse > =
serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-09 00:32:50 +02:00
2021-08-09 01:11:15 +02:00
return Ok ( resp . response ) ;
2021-08-09 00:32:50 +02:00
}
2021-08-10 05:08:51 +02:00
/// Host: api.steampowered.com
/// Endpoint: POST /ITwoFactorService/RemoveAuthenticator/v0001
2021-08-10 23:17:41 +02:00
pub fn remove_authenticator (
& self ,
revocation_code : String ,
) -> anyhow ::Result < RemoveAuthenticatorResponse > {
let params = hashmap! {
2022-06-19 20:42:07 +02:00
" steamid " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . steam_id . to_string ( ) ,
2021-08-10 05:08:51 +02:00
" steamguard_scheme " = > " 2 " . into ( ) ,
" revocation_code " = > revocation_code ,
2022-06-19 20:42:07 +02:00
" access_token " = > self . session . as_ref ( ) . unwrap ( ) . expose_secret ( ) . token . to_string ( ) ,
2021-08-10 05:08:51 +02:00
} ;
let resp = self
. post ( format! (
" {}/ITwoFactorService/RemoveAuthenticator/v0001 " ,
STEAM_API_BASE . to_string ( )
) )
. form ( & params )
. send ( ) ? ;
let text = resp . text ( ) ? ;
trace! ( " raw remove authenticator response: {} " , text ) ;
2021-08-10 23:17:41 +02:00
let resp : SteamApiResponse < RemoveAuthenticatorResponse > =
serde_json ::from_str ( text . as_str ( ) ) ? ;
2021-08-10 05:08:51 +02:00
return Ok ( resp . response ) ;
}
2021-08-08 00:47:39 +02:00
}