roblox_api/api/users/
v1.rs

1use serde::{Deserialize, Serialize};
2use strum::{EnumIter, IntoEnumIterator};
3
4use crate::{DateTime, Error, Paging, client::Client};
5
6pub const URL: &str = "https://users.roblox.com/v1";
7
8#[repr(u8)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
10pub enum Gender {
11    None = 1,
12    Male = 2,
13    Female = 3,
14}
15
16impl std::fmt::Display for Gender {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "{:?}", self)
19    }
20}
21
22impl TryFrom<&str> for Gender {
23    type Error = &'static str;
24
25    fn try_from(value: &str) -> Result<Self, Self::Error> {
26        for kind in Gender::iter() {
27            if kind.to_string().as_str() == value {
28                return Ok(kind);
29            }
30        }
31
32        Err("Failed to convert string to Gender")
33    }
34}
35
36impl Gender {
37    fn from_u8(value: u8) -> Self {
38        match value {
39            2 => Self::Male,
40            3 => Self::Female,
41            _ => Self::None,
42        }
43    }
44}
45
46#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
47pub struct ClientDetails {
48    pub id: u64,
49    pub name: String,
50    #[serde(rename = "displayName")]
51    pub display_name: String,
52}
53
54#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
55pub struct ClientAppLaunchInfo {
56    pub id: u64,
57    pub name: String,
58    #[serde(rename = "displayName")]
59    pub display_name: String,
60    #[serde(rename = "isPremium")]
61    pub is_premium: bool,
62    #[serde(rename = "countryCode")]
63    pub country_code: String,
64    #[serde(rename = "ageBracket")]
65    pub age_bracket: u64,
66}
67
68#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
69pub struct UserDetails {
70    pub id: u64,
71    pub name: String,
72    #[serde(rename = "displayName")]
73    pub display_name: String,
74    pub description: String,
75    pub created: DateTime,
76    #[serde(rename = "isBanned")]
77    pub is_terminated: bool,
78    #[serde(rename = "hasVerifiedBadge")]
79    pub is_verified: bool,
80}
81
82#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
83pub struct UserById {
84    pub id: u64,
85    pub name: String,
86    #[serde(rename = "displayName")]
87    pub display_name: String,
88    #[serde(rename = "hasVerifiedBadge")]
89    pub is_verified: bool,
90}
91
92#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
93pub struct UserByName {
94    pub id: u64,
95    pub name: String,
96    #[serde(rename = "requestedUsername")]
97    pub requested_name: String,
98    #[serde(rename = "displayName")]
99    pub display_name: String,
100    #[serde(rename = "hasVerifiedBadge")]
101    pub is_verified: bool,
102}
103
104#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
105pub struct UserByKeyword {
106    pub id: u64,
107    pub name: String,
108    #[serde(rename = "displayName")]
109    pub display_name: String,
110    #[serde(rename = "previousUsernames")]
111    pub previous_names: Vec<String>,
112    #[serde(rename = "hasVerifiedBadge")]
113    pub is_verified: bool,
114}
115
116#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
117pub struct NameHistory {
118    pub names: Vec<String>,
119    pub next_cursor: Option<String>,
120    pub previous_cursor: Option<String>,
121}
122
123#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
124pub struct UserSearchByKeyword {
125    #[serde(rename = "data")]
126    pub users: Vec<UserByKeyword>,
127    #[serde(rename = "nextPageCursor")]
128    pub next_cursor: Option<String>,
129    #[serde(rename = "previousPageCursor")]
130    pub previous_cursor: Option<String>,
131}
132
133pub async fn user_details(client: &mut Client, id: u64) -> Result<UserDetails, Error> {
134    let result = client
135        .requestor
136        .client
137        .get(format!("{URL}/users/{id}"))
138        .headers(client.requestor.default_headers.clone())
139        .send()
140        .await;
141
142    let response = client.validate_response(result).await?;
143    client.requestor.parse_json::<UserDetails>(response).await
144}
145
146pub async fn user_username_history(
147    client: &mut Client,
148    id: u64,
149    paging: Paging<'_>,
150) -> Result<NameHistory, Error> {
151    let limit = paging.limit.unwrap_or(10);
152    let sort_order = paging.order.unwrap_or_default().to_string();
153    let cursor = match paging.cursor {
154        Some(cursor) => format!("&cursor={cursor}"),
155        None => String::new(),
156    };
157
158    let result = client
159        .requestor
160        .client
161        .get(format!(
162            "{URL}/users/{id}/username-history?limit={limit}&sortOrder={sort_order}{cursor}"
163        ))
164        .headers(client.requestor.default_headers.clone())
165        .send()
166        .await;
167
168    // no they don't have anything else in here
169    #[derive(Debug, Deserialize)]
170    struct Username {
171        name: String,
172    }
173
174    #[derive(Debug, Deserialize)]
175    struct Response {
176        #[serde(rename = "data")]
177        names: Vec<Username>,
178        #[serde(rename = "nextPageCursor")]
179        next_cursor: Option<String>,
180        #[serde(rename = "previousPageCursor")]
181        previous_cursor: Option<String>,
182    }
183
184    let response = client.validate_response(result).await?;
185    let result = client.requestor.parse_json::<Response>(response).await?;
186
187    let names = result.names.iter().map(|x| x.name.clone()).collect();
188    Ok(NameHistory {
189        names,
190        next_cursor: result.next_cursor,
191        previous_cursor: result.previous_cursor,
192    })
193}
194
195pub async fn users_by_id(
196    client: &mut Client,
197    ids: &[u64],
198    exclude_terminated: bool,
199) -> Result<Vec<UserById>, Error> {
200    #[derive(Debug, Serialize)]
201    struct Request<'a> {
202        #[serde(rename = "userIds")]
203        ids: &'a [u64],
204        #[serde(rename = "excludeBannedUsers")]
205        exclude_terminated: bool,
206    }
207
208    let result = client
209        .requestor
210        .client
211        .post(format!("{URL}/users"))
212        .headers(client.requestor.default_headers.clone())
213        .json(&Request {
214            ids,
215            exclude_terminated,
216        })
217        .send()
218        .await;
219
220    #[derive(Debug, Deserialize)]
221    struct Response {
222        #[serde(rename = "data")]
223        users: Vec<UserById>,
224    }
225
226    let response = client.validate_response(result).await?;
227    Ok(client
228        .requestor
229        .parse_json::<Response>(response)
230        .await?
231        .users)
232}
233
234pub async fn users_by_name(
235    client: &mut Client,
236    names: &[&str],
237    exclude_terminated: bool,
238) -> Result<Vec<UserByName>, Error> {
239    #[derive(Debug, Serialize)]
240    struct Request<'a> {
241        #[serde(rename = "usernames")]
242        names: &'a [&'a str],
243        #[serde(rename = "excludeBannedUsers")]
244        exclude_terminated: bool,
245    }
246
247    let result = client
248        .requestor
249        .client
250        .post(format!("{URL}/usernames/users"))
251        .headers(client.requestor.default_headers.clone())
252        .json(&Request {
253            names,
254            exclude_terminated,
255        })
256        .send()
257        .await;
258
259    #[derive(Debug, Deserialize)]
260    struct Response {
261        #[serde(rename = "data")]
262        names: Vec<UserByName>,
263    }
264
265    let response = client.validate_response(result).await?;
266    Ok(client
267        .requestor
268        .parse_json::<Response>(response)
269        .await?
270        .names)
271}
272
273// this api seems to be dysfunctional, try using search::omni instead
274pub async fn search_by_keyword(
275    client: &mut Client,
276    keyword: &str,
277    session_id: Option<&str>,
278    paging: Paging<'_>,
279) -> Result<UserSearchByKeyword, Error> {
280    let limit = paging.limit.unwrap_or(10);
281    let cursor = match paging.cursor {
282        Some(cursor) => format!("&cursor={cursor}"),
283        None => String::new(),
284    };
285
286    let session_id = match session_id {
287        Some(session_id) => format!("&session_id={session_id}"),
288        None => String::new(),
289    };
290
291    let result = client
292        .requestor
293        .client
294        .get(format!(
295            "{URL}/users/search?keyword={keyword}&limit={limit}{cursor}{session_id}"
296        ))
297        .headers(client.requestor.default_headers.clone())
298        .send()
299        .await;
300
301    let response = client.validate_response(result).await?;
302    client
303        .requestor
304        .parse_json::<UserSearchByKeyword>(response)
305        .await
306}
307
308pub async fn authenticated_details(client: &mut Client) -> Result<ClientDetails, Error> {
309    let result = client
310        .requestor
311        .client
312        .get(format!("{URL}/users/authenticated"))
313        .headers(client.requestor.default_headers.clone())
314        .send()
315        .await;
316
317    let response = client.validate_response(result).await?;
318    client.requestor.parse_json::<ClientDetails>(response).await
319}
320
321pub async fn authenticated_age_bracket(client: &mut Client) -> Result<u64, Error> {
322    let result = client
323        .requestor
324        .client
325        .get(format!("{URL}/users/authenticated/age-bracket"))
326        .headers(client.requestor.default_headers.clone())
327        .send()
328        .await;
329
330    #[derive(Debug, Deserialize)]
331    struct Response {
332        #[serde(rename = "ageBracket")]
333        age_bracket: u64,
334    }
335
336    let response = client.validate_response(result).await?;
337    Ok(client
338        .requestor
339        .parse_json::<Response>(response)
340        .await?
341        .age_bracket)
342}
343
344pub async fn authenticated_country_code(client: &mut Client) -> Result<String, Error> {
345    let result = client
346        .requestor
347        .client
348        .get(format!("{URL}/users/authenticated/country-code"))
349        .headers(client.requestor.default_headers.clone())
350        .send()
351        .await;
352
353    #[derive(Debug, Deserialize)]
354    struct Response {
355        #[serde(rename = "countryCode")]
356        country_code: String,
357    }
358
359    let response = client.validate_response(result).await?;
360    Ok(client
361        .requestor
362        .parse_json::<Response>(response)
363        .await?
364        .country_code)
365}
366
367pub async fn authenticated_roles(client: &mut Client) -> Result<Vec<String>, Error> {
368    let result = client
369        .requestor
370        .client
371        .get(format!("{URL}/users/authenticated/roles"))
372        .headers(client.requestor.default_headers.clone())
373        .send()
374        .await;
375
376    #[derive(Debug, Deserialize)]
377    struct Response {
378        roles: Vec<String>,
379    }
380
381    let response = client.validate_response(result).await?;
382    Ok(client
383        .requestor
384        .parse_json::<Response>(response)
385        .await?
386        .roles)
387}
388
389pub async fn authenticated_app_launch_info(
390    client: &mut Client,
391) -> Result<ClientAppLaunchInfo, Error> {
392    let result = client
393        .requestor
394        .client
395        .get(format!("{URL}/users/authenticated/app-launch-info"))
396        .headers(client.requestor.default_headers.clone())
397        .send()
398        .await;
399
400    let response = client.validate_response(result).await?;
401    client
402        .requestor
403        .parse_json::<ClientAppLaunchInfo>(response)
404        .await
405}
406
407pub async fn birthdate(client: &mut Client) -> Result<DateTime, Error> {
408    let result = client
409        .requestor
410        .client
411        .get(format!("{URL}/birthdate"))
412        .headers(client.requestor.default_headers.clone())
413        .send()
414        .await;
415
416    #[derive(Debug, Deserialize)]
417    struct Response {
418        #[serde(rename = "birthDay")]
419        day: u8,
420        #[serde(rename = "birthMonth")]
421        month: u8,
422        #[serde(rename = "birthYear")]
423        year: i32,
424    }
425
426    let response = client.validate_response(result).await?;
427    let birthdate = client.requestor.parse_json::<Response>(response).await?;
428
429    Ok(DateTime::from_ymd(
430        birthdate.year,
431        birthdate.month,
432        birthdate.day,
433    ))
434}
435
436// According to documentation there's supposed to be a password,
437// yet the roblox website doesn't use it..
438pub async fn set_birthdate(
439    client: &mut Client,
440    birthdate: DateTime,
441    //password: &str,
442) -> Result<(), Error> {
443    #[derive(Debug, Serialize)]
444    struct Request {
445        #[serde(rename = "birthDay")]
446        day: u8,
447        #[serde(rename = "birthMonth")]
448        month: u8,
449        #[serde(rename = "birthYear")]
450        year: i32,
451        //pub password: &'a str,
452    }
453
454    let result = client
455        .requestor
456        .client
457        .post(format!("{URL}/birthdate"))
458        .headers(client.requestor.default_headers.clone())
459        .json(&Request {
460            day: birthdate.day(),
461            month: birthdate.month(),
462            year: birthdate.year(),
463            //password,
464        })
465        .send()
466        .await;
467
468    client.validate_response(result).await?;
469    Ok(())
470}
471
472pub async fn description(client: &mut Client) -> Result<String, Error> {
473    let result = client
474        .requestor
475        .client
476        .get(format!("{URL}/description"))
477        .headers(client.requestor.default_headers.clone())
478        .send()
479        .await;
480
481    #[derive(Debug, Deserialize)]
482    struct Response {
483        #[serde(rename = "description")]
484        value: String,
485    }
486
487    let response = client.validate_response(result).await?;
488    let description = client.requestor.parse_json::<Response>(response).await?;
489
490    Ok(description.value)
491}
492
493pub async fn set_description(client: &mut Client, description: &str) -> Result<(), Error> {
494    #[derive(Debug, Serialize)]
495    struct Request<'a> {
496        #[serde(rename = "description")]
497        value: &'a str,
498    }
499
500    let result = client
501        .requestor
502        .client
503        .post(format!("{URL}/description"))
504        .headers(client.requestor.default_headers.clone())
505        .json(&Request { value: description })
506        .send()
507        .await;
508
509    client.validate_response(result).await?;
510    Ok(())
511}
512
513pub async fn gender(client: &mut Client) -> Result<Gender, Error> {
514    let result = client
515        .requestor
516        .client
517        .get(format!("{URL}/gender"))
518        .headers(client.requestor.default_headers.clone())
519        .send()
520        .await;
521
522    #[derive(Debug, Deserialize)]
523    struct Response {
524        #[serde(rename = "gender")]
525        value: u8,
526    }
527
528    let response = client.validate_response(result).await?;
529    let gender: Response = client.requestor.parse_json(response).await?;
530
531    Ok(Gender::from_u8(gender.value))
532}
533
534pub async fn set_gender(client: &mut Client, gender: Gender) -> Result<(), Error> {
535    #[derive(Debug, Serialize)]
536    struct Request {
537        #[serde(rename = "gender")]
538        value: u8,
539    }
540
541    let result = client
542        .requestor
543        .client
544        .post(format!("{URL}/gender"))
545        .headers(client.requestor.default_headers.clone())
546        .json(&Request {
547            value: gender as u8,
548        })
549        .send()
550        .await;
551
552    client.validate_response(result).await?;
553    Ok(())
554}
555
556pub async fn validate_display_name(
557    client: &mut Client,
558    display_name: &str,
559    birthdate: DateTime,
560) -> Result<(), Error> {
561    let result = client
562        .requestor
563        .client
564        .get(format!(
565            "{URL}/display-names/validate?displayName={display_name}&birthdate={}",
566            birthdate
567        ))
568        .headers(client.requestor.default_headers.clone())
569        .send()
570        .await;
571
572    client.validate_response(result).await?;
573    Ok(())
574}
575
576// TODO: I don't know what `id` is for, as this api only seems to be used for the client only,
577// there's also currently no way to require id from Client, perhaps we should authenticate
578// on from_cookie method, and store the ClientDetails in the Client
579pub async fn validate_display_name_by_id(
580    client: &mut Client,
581    id: u64,
582    display_name: &str,
583) -> Result<(), Error> {
584    let result = client
585        .requestor
586        .client
587        .get(format!(
588            "{URL}/users/{id}/display-names/validate?displayName={display_name}"
589        ))
590        .headers(client.requestor.default_headers.clone())
591        .send()
592        .await;
593
594    client.validate_response(result).await?;
595    Ok(())
596}
597
598// TODO: I don't know what `id` is for, as this api only seems to be used for the client only,
599// there's also currently no way to require id from Client, perhaps we should authenticate
600// on from_cookie method, and store the ClientDetails in the Client
601pub async fn set_display_name(
602    client: &mut Client,
603    id: u64,
604    display_name: &str,
605) -> Result<(), Error> {
606    #[derive(Debug, Serialize)]
607    struct Request<'a> {
608        #[serde(rename = "newDisplayName")]
609        display_name: &'a str,
610    }
611
612    let result = client
613        .requestor
614        .client
615        .patch(format!(
616            "{URL}/users/{id}/display-names?displayName={display_name}"
617        ))
618        .headers(client.requestor.default_headers.clone())
619        .json(&Request { display_name })
620        .send()
621        .await;
622
623    client.validate_response(result).await?;
624    Ok(())
625}