1use reqwest::Method;
2use serde::{Deserialize, Serialize, de::DeserializeOwned};
3
4use crate::{Error, Paging, client::Client};
5
6pub const URL: &str = "https://avatar.roblox.com/v1";
7
8pub type ColorId = u8;
9
10#[repr(u8)]
11#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
12pub enum AvatarType {
13 R6 = 1,
14 R15 = 2,
15}
16
17#[repr(u8)]
18#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
19pub enum MorphAvatarType {
20 MorphR6 = 1,
21 MorphR15 = 2,
22}
23
24#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
25#[serde(rename_all = "camelCase")]
26pub struct AvatarScales {
27 pub height: f32,
28 pub width: f32,
29 pub head: f32,
30 pub depth: f32,
31 pub proportion: f32,
32 pub body_type: f32,
33}
34
35#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
36pub struct BodyColors {
37 #[serde(rename = "headColorId")]
38 pub head: ColorId,
39 #[serde(rename = "torsoColorId")]
40 pub torso: ColorId,
41 #[serde(rename = "rightArmColorId")]
42 pub right_arm: ColorId,
43 #[serde(rename = "leftArmColorId")]
44 pub left_arm: ColorId,
45 #[serde(rename = "rightLegColorId")]
46 pub right_leg: ColorId,
47 #[serde(rename = "leftLegColorId")]
48 pub left_leg: ColorId,
49}
50
51#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
52pub struct AssetType {
53 pub id: u8,
55 pub name: String,
56}
57
58#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
59pub struct AssetMeta {
60 pub version: u8,
61
62 pub order: u16,
63 pub puffiness: f32,
64}
65
66#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
67#[serde(rename_all = "camelCase")]
68pub struct Asset {
69 pub id: u64,
70 pub name: String,
71 #[serde(rename = "assetType")]
72 pub kind: AssetType,
73 pub current_version_id: u64,
74 pub meta: Option<AssetMeta>,
75}
76
77#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
78#[serde(rename_all = "camelCase")]
79pub struct Emote {
80 #[serde(rename = "assetId")]
81 pub id: u64,
82 #[serde(rename = "assetName")]
83 pub name: String,
84 pub position: u8,
85}
86
87#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
88#[serde(rename_all = "camelCase")]
89pub struct Outfit {
90 pub id: u64,
91 pub name: String,
92 pub is_editable: bool,
93 pub outfit_type: Option<()>,
94}
95
96#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
97#[serde(rename_all = "camelCase")]
98pub struct AvatarResponse {
99 #[serde(rename = "playerAvatarType")]
100 pub kind: AvatarType,
101 pub assets: Vec<Asset>,
102 pub scales: AvatarScales,
103 pub body_colors: BodyColors,
104 pub default_pants_applied: bool,
105 pub default_shirt_applied: bool,
106 pub emotes: Vec<Emote>,
107}
108
109#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
110#[serde(rename_all = "camelCase")]
111pub struct OutfitsResponse {
112 #[serde(rename = "data")]
113 pub outfits: Vec<Outfit>,
114 pub total: u64,
115 pub filtered_count: u64,
116}
117
118#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
119#[serde(rename_all = "camelCase")]
120pub struct OutfitDetails {
121 pub id: u64,
122 pub name: String,
123 pub universe_id: u64,
124 pub assets: Vec<Asset>,
125 pub body_colors: BodyColors,
126 #[serde(rename = "scale")]
127 pub scales: AvatarScales,
128 #[serde(rename = "playerAvatarType")]
129 pub avatar_type: AvatarType,
130 pub outfit_type: String, pub is_editable: bool,
132 pub moderation_status: Option<String>,
133}
134
135#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
136#[serde(rename_all = "camelCase")]
137pub struct UniverseAvatarSettings {
138 #[serde(rename = "gameAvatarType")]
139 pub avatar_type: MorphAvatarType,
140 pub avatar_body_type: String, pub avatar_collision_type: String, pub joint_positioning_type: String, pub avatar_min_scales: AvatarScales,
145 pub avatar_max_scales: AvatarScales,
146 pub avatar_asset_overrides: Vec<Option<()>>,
147
148 pub message: String,
149 pub moderation_status: Option<String>,
150
151 pub allow_custom_animations: String, }
153
154async fn generic_request<'a, R: Serialize, T: DeserializeOwned>(
155 client: &mut Client,
156 method: Method,
157 path: &str,
158 request: Option<&'a R>,
159 query: Option<&'a [(&'a str, &'a str)]>,
160) -> Result<T, Error> {
161 let mut builder = client
162 .requestor
163 .client
164 .request(method, format!("{URL}/{path}"))
165 .headers(client.requestor.default_headers.clone());
166
167 if let Some(request) = request {
168 builder = builder.json(&request);
169 }
170
171 if let Some(query) = query {
172 builder = builder.query(&query);
173 }
174
175 let response = client.validate_response(builder.send().await).await?;
176 client.requestor.parse_json::<T>(response).await
177}
178
179pub async fn user_avatar(client: &mut Client, id: u64) -> Result<AvatarResponse, Error> {
181 generic_request::<(), AvatarResponse>(
182 client,
183 Method::GET,
184 &format!("/users/{id}/avatar"),
185 None,
186 None,
187 )
188 .await
189}
190
191pub async fn user_currently_wearing(client: &mut Client, id: u64) -> Result<Vec<u64>, Error> {
193 #[derive(Deserialize)]
194 struct Response {
195 #[serde(rename = "assetIds")]
196 ids: Vec<u64>,
197 }
198
199 Ok(generic_request::<(), Response>(
200 client,
201 Method::GET,
202 &format!("/users/{id}/currently-wearing"),
203 None,
204 None,
205 )
206 .await?
207 .ids)
208}
209
210pub async fn avatar_set_wearing_assets(
216 client: &mut Client,
217 assets: Vec<u64>,
218) -> Result<bool, Error> {
219 #[derive(Serialize)]
220 #[serde(rename_all = "camelCase")]
221 struct Request<'a> {
222 asset_ids: &'a [u64],
223 }
224
225 #[derive(Deserialize)]
226 pub struct Response {
227 pub success: bool,
228 }
229
230 Ok(generic_request::<Request, Response>(
231 client,
232 Method::POST,
233 &format!("/avatar/set-wearing-assets"),
234 Some(&Request { asset_ids: &assets }),
235 None,
236 )
237 .await?
238 .success)
239}
240
241pub async fn avatar_set_type(client: &mut Client, kind: AvatarType) -> Result<bool, Error> {
243 #[derive(Serialize)]
244 struct Request {
245 #[serde(rename = "playerAvatarType")]
246 avatar_type: AvatarType,
247 }
248
249 #[derive(Deserialize)]
250 struct Response {
251 success: bool,
252 }
253
254 Ok(generic_request::<Request, Response>(
255 client,
256 Method::POST,
257 &format!("/avatar/set-player-avatar-type"),
258 Some(&Request { avatar_type: kind }),
259 None,
260 )
261 .await?
262 .success)
263}
264
265pub async fn avatar_set_body_colors(
267 client: &mut Client,
268 colors: BodyColors,
269) -> Result<bool, Error> {
270 #[derive(Deserialize)]
271 struct Response {
272 success: bool,
273 }
274
275 Ok(generic_request::<BodyColors, Response>(
276 client,
277 Method::POST,
278 &format!("/avatar/set-body-colors"),
279 Some(&colors),
280 None,
281 )
282 .await?
283 .success)
284}
285
286pub async fn avatar_set_scales(client: &mut Client, scales: AvatarScales) -> Result<bool, Error> {
288 #[derive(Deserialize)]
289 struct Response {
290 success: bool,
291 }
292
293 Ok(generic_request::<AvatarScales, Response>(
294 client,
295 Method::POST,
296 &format!("/avatar/set-scales"),
297 Some(&scales),
298 None,
299 )
300 .await?
301 .success)
302}
303
304pub async fn user_outfits(
306 client: &mut Client,
307 id: u64,
308 paging: Paging<'_>,
309 is_editable: bool,
310 ) -> Result<OutfitsResponse, Error> {
312 let limit = paging.limit.unwrap_or(25).to_string();
313 let cursor = paging.cursor.unwrap_or("1");
314
315 generic_request::<(), OutfitsResponse>(
316 client,
317 Method::GET,
318 &format!("/users/{id}/outfits"),
319 None,
320 Some(&[
321 ("page", cursor),
322 ("itemsPerPage", &limit),
323 ("isEditable", is_editable.to_string().as_str()),
324 ]),
325 )
326 .await
327}
328
329pub async fn outfit_details(client: &mut Client, id: u64) -> Result<OutfitDetails, Error> {
331 generic_request::<(), OutfitDetails>(
332 client,
333 Method::GET,
334 &format!("/outfits/{id}/details"),
335 None,
336 None,
337 )
338 .await
339}
340
341pub async fn remove_outfit(client: &mut Client, id: u64) -> Result<bool, Error> {
343 #[derive(Deserialize)]
344 struct Response {
345 success: bool,
346 }
347
348 Ok(generic_request::<(), Response>(
349 client,
350 Method::POST,
351 &format!("/outfits/{id}/delete"),
352 None,
353 None,
354 )
355 .await?
356 .success)
357}
358
359pub async fn universe_avatar_settings(
363 client: &mut Client,
364 id: u64,
365) -> Result<UniverseAvatarSettings, Error> {
366 generic_request::<(), UniverseAvatarSettings>(
367 client,
368 Method::GET,
369 &format!("/users/{id}/avatar"),
370 None,
371 None,
372 )
373 .await
374}