Skip to main content

roblox_api/
client.rs

1use reqwest::{
2    Method, Response,
3    header::{self, HeaderMap, HeaderValue},
4};
5use serde::{Serialize, de::DeserializeOwned};
6
7use crate::{ApiError, Error, ratelimit::Ratelimit};
8
9#[derive(Default)]
10pub struct Cookie(String);
11
12impl std::fmt::Display for Cookie {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        write!(f, "{}", self.0)
15    }
16}
17
18impl From<&str> for Cookie {
19    fn from(value: &str) -> Self {
20        Self(format!(".ROBLOSECURITY={}", value))
21    }
22}
23
24#[derive(Default, Debug)]
25pub struct ClientRequestor {
26    pub(crate) client: reqwest::Client,
27    pub(crate) default_headers: HeaderMap,
28    pub(crate) ratelimit: Option<Ratelimit>,
29}
30
31#[derive(Default, Debug)]
32pub struct Client {
33    pub requestor: ClientRequestor,
34}
35
36impl Client {
37    pub fn from_cookie(cookie: Cookie) -> Self {
38        let client = reqwest::Client::new();
39        let mut default_headers = HeaderMap::new();
40
41        default_headers.insert(
42            header::USER_AGENT,
43            HeaderValue::from_str("Roblox/WinInet").unwrap(),
44        );
45
46        default_headers.insert(
47            header::COOKIE,
48            HeaderValue::from_str(&cookie.to_string()).unwrap(),
49        );
50
51        // For some reason some APIs error if not set
52        default_headers.append(
53            header::COOKIE,
54            HeaderValue::from_str("RBXEventTrackerV2=&browserid=2").unwrap(),
55        );
56
57        Client {
58            requestor: ClientRequestor {
59                client,
60                default_headers,
61                ratelimit: None,
62            },
63        }
64    }
65
66    #[deprecated = "Failed requests due to token validation are now automatically retried"]
67    pub async fn ensure_token(&mut self) -> Result<(), Error> {
68        Ok(())
69    }
70
71    pub async fn ratelimits(&self) -> Option<Ratelimit> {
72        self.requestor.ratelimits().await
73    }
74
75    // TODO: test if account is terminated
76    // TODO: add reactivate account function
77    // pub async fn test_account_status() {}
78}
79
80pub(crate) struct ResponseWrapped(Response);
81impl ResponseWrapped {
82    pub(crate) async fn json<T: DeserializeOwned>(self) -> Result<T, Error> {
83        Ok(self.0.json::<T>().await?)
84    }
85
86    pub(crate) async fn bytes(self) -> Result<Vec<u8>, Error> {
87        let bytes = self.0.bytes().await;
88        match bytes {
89            Ok(bytes) => Ok(bytes.to_vec()),
90            Err(error) => Err(Error::ReqwestError(error)),
91        }
92    }
93}
94
95impl ClientRequestor {
96    pub(crate) async fn parse_json<T: DeserializeOwned>(
97        &self,
98        response: Response,
99    ) -> Result<T, Error> {
100        Ok(response.json::<T>().await?)
101    }
102
103    pub(crate) async fn request<'a, R: Serialize>(
104        &mut self,
105        method: Method,
106        url: &str,
107        request: Option<&'a R>,
108        query: Option<&'a [(&'a str, &'a str)]>,
109        headers: Option<HeaderMap>,
110    ) -> Result<ResponseWrapped, Error> {
111        let mut builder = self
112            .client
113            .request(method.clone(), url)
114            .headers(headers.clone().unwrap_or(self.default_headers.clone()));
115
116        // Even though sending None works, it might get serialized as null in json, which is a waste of bytes
117        if let Some(request) = request {
118            builder = builder.json(&request);
119        }
120
121        if let Some(query) = query {
122            builder = builder.query(&query);
123        }
124
125        let response = self.validate_response(builder.send().await).await;
126
127        match response {
128            Err(Error::ApiError(ApiError::TokenValidation)) => {
129                let mut builder = self
130                    .client
131                    .request(method, url)
132                    .headers(headers.unwrap_or(self.default_headers.clone()));
133
134                if let Some(request) = request {
135                    builder = builder.json(&request);
136                }
137
138                if let Some(query) = query {
139                    builder = builder.query(&query);
140                }
141
142                let response = self.validate_response(builder.send().await).await?;
143                Ok(ResponseWrapped(response))
144            }
145
146            response => Ok(ResponseWrapped(response?)),
147        }
148    }
149}