Skip to main content

roblox_api/api/users/
v1.rs

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