roblox_api/
challenge.rs

1use base64::{Engine, prelude::BASE64_STANDARD};
2use reqwest::header::HeaderValue;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    Error,
7    api::challenge,
8    client::{Client, ClientRequestor},
9};
10
11pub(crate) const CHALLENGE_ID_HEADER: &str = "rblx-challenge-id";
12pub(crate) const CHALLENGE_TYPE_HEADER: &str = "rblx-challenge-type";
13pub(crate) const CHALLENGE_METADATA_HEADER: &str = "rblx-challenge-metadata";
14
15#[derive(Clone, Copy, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
16pub enum ActionType {
17    #[default]
18    Unknown = 0,
19    Login,
20    RobuxSpend,
21    ItemTrade,
22    Resale,
23    PasswordReset,
24    RevertAccount,
25    Generic,
26    GenericWithRecoveryCodes,
27}
28
29impl std::fmt::Display for ActionType {
30    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31        write!(f, "{:?}", self)
32    }
33}
34
35#[derive(Clone, Copy, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
36#[serde(rename_all = "lowercase")]
37pub enum ChallengeType {
38    #[default]
39    Generic,
40    Captcha,
41    // This requires doing javascript challenges, however completing them is upto the user of this api
42    Chef,
43    #[serde(rename = "twostepverification")]
44    TwoStepVerification,
45    // I'm not sure what this is, however I say it being referenced in some places
46    Reauthentication,
47    // I'm not sure what this is, however I say it being referenced in some places
48    #[serde(rename = "security-questions")]
49    SecurityQuestions,
50}
51
52impl From<&str> for ChallengeType {
53    fn from(value: &str) -> Self {
54        match value {
55            "generic" => Self::Generic,
56            "captcha" => Self::Captcha,
57            "chef" => Self::Chef,
58            "twostepverification" => Self::TwoStepVerification,
59            "reauthentication" => Self::Reauthentication,
60            "security-questions" => Self::SecurityQuestions,
61
62            _ => Self::Generic,
63        }
64    }
65}
66
67impl std::fmt::Display for ChallengeType {
68    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69        write!(f, "{:?}", self)
70    }
71}
72
73#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase")]
75pub struct ChallengeMetadata {
76    pub(crate) user_id: String,
77    #[serde(rename = "challengeId")]
78    pub server_challenge_id: String,
79    pub action_type: ActionType,
80    pub(crate) remember_device: bool,
81}
82
83#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
84#[serde(rename_all = "camelCase")]
85pub struct ChefChallengeMetadata {
86    pub(crate) user_id: String,
87    #[serde(rename = "challengeId")]
88    pub server_challenge_id: String,
89    pub expected_symbols: Vec<String>,
90    pub script_identifiers: Vec<String>,
91}
92
93#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
94pub struct Challenge {
95    pub id: String,
96    pub kind: ChallengeType,
97    pub metadata: ChallengeMetadata,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub(crate) struct ChallengeMetadataRequest {
103    pub(crate) verification_token: String,
104    pub(crate) challenge_id: String,
105    pub(crate) action_type: ActionType,
106    pub(crate) remember_device: bool,
107}
108
109impl Client {
110    // the name is misleading, there's no queue, also this function is kinda ugly to use,
111    // perhaps it should be reworked
112    pub async fn queue_challenge(
113        &mut self,
114        challenge: &Challenge,
115        verification_token: &str,
116    ) -> Result<(), Error> {
117        // the challenge requires this api call, otherwise it fails
118        challenge::v1::continue_challenge(self, challenge, verification_token).await?;
119        self.requestor
120            .queue_challenge(challenge, verification_token)
121            .await
122    }
123}
124
125impl ClientRequestor {
126    async fn queue_challenge(
127        &mut self,
128        challenge: &Challenge,
129        verification_token: &str,
130    ) -> Result<(), Error> {
131        self.default_headers.insert(
132            CHALLENGE_ID_HEADER,
133            HeaderValue::from_str(&challenge.id).unwrap(),
134        );
135
136        self.default_headers.insert(
137            CHALLENGE_TYPE_HEADER,
138            HeaderValue::from_str(&challenge.kind.to_string()).unwrap(),
139        );
140
141        let metadata_b64 = BASE64_STANDARD.encode(
142            serde_json::to_vec(
143                &(ChallengeMetadataRequest {
144                    verification_token: verification_token.to_string(),
145                    challenge_id: challenge.metadata.server_challenge_id.clone(),
146                    action_type: challenge.metadata.action_type,
147                    remember_device: challenge.metadata.remember_device,
148                }),
149            )
150            .unwrap(),
151        );
152
153        self.default_headers.insert(
154            CHALLENGE_METADATA_HEADER,
155            HeaderValue::from_str(&metadata_b64).unwrap(),
156        );
157
158        Ok(())
159    }
160
161    pub(crate) fn remove_challenge(&mut self) {
162        self.default_headers.remove(CHALLENGE_ID_HEADER);
163        self.default_headers.remove(CHALLENGE_TYPE_HEADER);
164        self.default_headers.remove(CHALLENGE_METADATA_HEADER);
165    }
166}