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