roblox_api/
challenge.rs

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