roboat/client.rs
1use crate::users::ClientUserInformation;
2use crate::RoboatError;
3use reqwest::header::HeaderValue;
4// We use tokio's version of rwlock so that readers to not starve writers on linux.
5use tokio::sync::RwLock;
6
7/// A client used for making requests to the Roblox API.
8///
9/// The client stores the roblosecurity cookie, X-CSRF-TOKEN header, and an HTTPS client to send web
10/// requests. The client also caches the user id, username, and display name of the user.
11///
12/// Constructed using a [`ClientBuilder`].
13///
14/// # Construction Examples
15///
16/// ## Without Roblosecurity or a Custom Reqwest Client
17/// ```
18/// use roboat::ClientBuilder;
19///
20/// let client = ClientBuilder::new().build();
21/// ```
22///
23/// ## With a Roblosecurity
24/// ```
25/// use roboat::ClientBuilder;
26///
27/// const ROBLOSECURITY: &str = "roblosecurity";
28///
29/// let client = ClientBuilder::new().roblosecurity(ROBLOSECURITY.to_string()).build();
30/// ```
31///
32/// ## With a Custom Reqwest Client
33/// ```
34/// use roboat::ClientBuilder;
35///
36/// let reqwest_client = reqwest::Client::new();
37/// let client = ClientBuilder::new().reqwest_client(reqwest_client).build();
38/// ```
39///
40/// ## With a Roblosecurity and a Custom Reqwest Client
41/// ```
42/// use roboat::ClientBuilder;
43///
44/// const ROBLOSECURITY: &str = "roblosecurity";
45///
46/// let reqwest_client = reqwest::Client::new();
47/// let client = ClientBuilder::new().roblosecurity(ROBLOSECURITY.to_string()).reqwest_client(reqwest_client).build();
48/// ```
49///
50/// # Standard Errors
51/// The errors that can be returned by any of `Client`'s methods are:
52/// - [`RoboatError::TooManyRequests`]
53/// - [`RoboatError::InternalServerError`]
54/// - [`RoboatError::BadRequest`]
55/// - [`RoboatError::UnknownRobloxErrorCode`]
56/// - [`RoboatError::UnidentifiedStatusCode`]
57/// - [`RoboatError::ReqwestError`]
58///
59/// # Auth Required Errors
60/// The errors that can be returned by any of `Client`'s methods that require authentication are:
61/// - [`RoboatError::InvalidRoblosecurity`]
62/// - [`RoboatError::RoblosecurityNotSet`]
63///
64/// # X-CSRF-TOKEN Required Errors
65/// The errors that can be returned by any of `Client`'s methods that require the X-CSRF-TOKEN header are:
66/// - [`RoboatError::InvalidXcsrf`]
67/// - [`RoboatError::XcsrfNotReturned`]
68///
69/// # 2-Factor Authentication / Captcha Required Errors
70/// The errors that can be returned by any of `Client`'s methods that require 2-factor authentication or a captcha are:
71/// - [`RoboatError::ChallengeRequired`]
72/// - [`RoboatError::UnknownStatus403Format`]
73#[derive(Debug, Default)]
74pub struct Client {
75 /// The full cookie that includes the roblosecurity token.
76 pub(crate) cookie_string: Option<HeaderValue>,
77 /// The field holding the value for the X-CSRF-TOKEN header used in and returned by endpoints.
78 pub(crate) xcsrf: RwLock<String>,
79 /// Holds the user id, username, and display name of the user.
80 pub(crate) user_information: RwLock<Option<ClientUserInformation>>,
81 /// A Reqwest HTTP client used to send web requests.
82 pub(crate) reqwest_client: reqwest::Client,
83}
84
85/// A builder used for constructing a [`Client`]. Constructed using [`ClientBuilder::new`].
86#[derive(Clone, Debug, Default)]
87pub struct ClientBuilder {
88 roblosecurity: Option<String>,
89 reqwest_client: Option<reqwest::Client>,
90}
91
92impl Client {
93 /// Returns the user id of the user. If the user id is not cached, it will be fetched from Roblox first.
94 ///
95 /// The user id should be the only thing used to differentiate between accounts as
96 /// username and display name can change.
97 pub async fn user_id(&self) -> Result<u64, RoboatError> {
98 let guard = self.user_information.read().await;
99 let user_information_opt = &*guard;
100
101 match user_information_opt {
102 Some(user_information) => Ok(user_information.user_id),
103 None => {
104 // Drop the read lock as the writer lock will be requested.
105 drop(guard);
106
107 let user_info = self.user_information_internal().await?;
108 Ok(user_info.user_id)
109 }
110 }
111 }
112
113 /// Returns the username of the user. If the username is not cached, it will be fetched from Roblox first.
114 ///
115 /// Username can change (although rarely). For this reason only user id should be used for differentiating accounts.
116 pub async fn username(&self) -> Result<String, RoboatError> {
117 let guard = self.user_information.read().await;
118 let user_information_opt = &*guard;
119
120 match user_information_opt {
121 Some(user_information) => Ok(user_information.username.clone()),
122 None => {
123 // Drop the read lock as the writer lock will be requested.
124 drop(guard);
125
126 let user_info = self.user_information_internal().await?;
127 Ok(user_info.username)
128 }
129 }
130 }
131
132 /// Returns the display name of the user. If the display name is not cached, it will be fetched from Roblox first.
133 ///
134 /// Display name can change. For this reason only user id should be used for differentiating accounts.
135 pub async fn display_name(&self) -> Result<String, RoboatError> {
136 let guard = self.user_information.read().await;
137 let user_information_opt = &*guard;
138
139 match user_information_opt {
140 Some(user_information) => Ok(user_information.display_name.clone()),
141 None => {
142 // Drop the read lock as the writer lock will be requested.
143 drop(guard);
144
145 let user_info = self.user_information_internal().await?;
146 Ok(user_info.display_name)
147 }
148 }
149 }
150
151 /// Used in [`Client::user_information_internal`]. This is implemented in the client
152 /// module as we do not want other modules to have to interact with the rwlock directly.
153 pub(crate) async fn set_user_information(&self, user_information: ClientUserInformation) {
154 *self.user_information.write().await = Some(user_information);
155 }
156
157 /// Sets the xcsrf token of the client. Remember to .await this method.
158 pub(crate) async fn set_xcsrf(&self, xcsrf: String) {
159 *self.xcsrf.write().await = xcsrf;
160 }
161
162 /// Returns a copy of the xcsrf stored in the client. Remember to .await this method.
163 pub(crate) async fn xcsrf(&self) -> String {
164 self.xcsrf.read().await.clone()
165 }
166
167 /// Returns a copy of the cookie string stored in the client.
168 /// If the roblosecurity has not been set, [`RoboatError::RoblosecurityNotSet`] is returned.
169 pub(crate) fn cookie_string(&self) -> Result<HeaderValue, RoboatError> {
170 let cookie_string_opt = &self.cookie_string;
171
172 match cookie_string_opt {
173 Some(cookie) => Ok(cookie.clone()),
174 None => Err(RoboatError::RoblosecurityNotSet),
175 }
176 }
177}
178
179impl ClientBuilder {
180 /// Creates a new [`ClientBuilder`].
181 pub fn new() -> Self {
182 Self::default()
183 }
184
185 /// Sets the roblosecurity for the client.
186 ///
187 /// # Example
188 /// ```rust
189 /// use roboat::ClientBuilder;
190 ///
191 /// const ROBLOSECURITY: &str = "roblosecurity";
192 ///
193 /// let client = ClientBuilder::new().roblosecurity(ROBLOSECURITY.to_string()).build();
194 /// ```
195 pub fn roblosecurity(mut self, roblosecurity: String) -> Self {
196 self.roblosecurity = Some(roblosecurity);
197 self
198 }
199
200 /// Sets the [`reqwest::Client`] for the client.
201 ///
202 /// # Example
203 /// ```rust
204 /// use roboat::ClientBuilder;
205 ///
206 /// let reqwest_client = reqwest::Client::new();
207 /// let client = ClientBuilder::new().reqwest_client(reqwest_client).build();
208 /// ```
209 pub fn reqwest_client(mut self, reqwest_client: reqwest::Client) -> Self {
210 self.reqwest_client = Some(reqwest_client);
211 self
212 }
213
214 /// Builds the [`Client`]. This consumes the builder.
215 ///
216 /// # Example
217 /// ```rust
218 /// use roboat::ClientBuilder;
219 ///
220 /// let client = ClientBuilder::new().build();
221 /// ```
222 pub fn build(self) -> Client {
223 Client {
224 cookie_string: self
225 .roblosecurity
226 .as_ref()
227 .map(|x| create_cookie_string_header(x)),
228 reqwest_client: self.reqwest_client.unwrap_or_default(),
229 ..Default::default()
230 }
231 }
232}
233
234fn create_cookie_string_header(roblosecurity: &str) -> HeaderValue {
235 // We panic here because I really really really hope that nobody is using invalid characters in their roblosecurity.
236 let mut header = HeaderValue::from_str(&format!(".ROBLOSECURITY={}", roblosecurity))
237 .expect("Invalid roblosecurity characters.");
238
239 header.set_sensitive(true);
240
241 header
242}