1use serde::{Deserialize, Serialize, Serializer};
2use strum::{EnumIter, IntoEnumIterator};
3use strum_macros::{Display, EnumString};
4
5use crate::{Error, client::Client};
6
7pub const URL: &str = "https://thumbnails.roblox.com/v1";
8
9#[derive(Clone, Debug, Deserialize, PartialEq, Eq, EnumIter)]
10pub enum ThumbnailSize {
11 S30x30,
12 S48x48,
13 S50x50,
14 S60x60,
15 S75x75,
16
17 S100x100,
18 S110x110,
19 S128x128,
20 S140x140,
21 S150x150,
22 S180x180,
23 S250x250,
24 S256x256,
25 S352x352,
26 S420x420,
27 S512x512,
28 S720x720,
29
30 S256x144,
31 S384x216,
32 S480x270,
33 S576x324,
34 S768x432,
35
36 S1200x80,
37 S1440x456,
38}
39
40#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
41pub enum ThumbnailFormat {
42 #[default]
43 Png,
44 Jpeg,
45 Webp,
46}
47
48#[derive(Clone, Default, Debug, PartialEq, Eq, Display, EnumString)]
49pub enum ReturnPolicy {
50 #[default]
51 PlaceHolder,
52 ForcePlaceHolder,
53 AutoGenerated,
54 ForceAutoGenerated,
55}
56
57#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
58pub enum ThumbnailState {
59 Pending,
60 Blocked,
61 Completed,
62}
63
64#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
65pub enum ThumbnailVersion {
66 TN3,
67}
68
69#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString)]
70pub enum ThumbnailRequestType {
71 Avatar = 1,
72 AvatarHeadShot,
73 GameIcon,
74 BadgeIcon,
75 GameThumbnail,
76 GamePass,
77 Asset,
78 BundleThumbnail,
79 Outfit,
80 GroupIcon,
81 DeveloperProduct,
82 AutoGeneratedAsset,
83 AvatarBust,
84 PlaceIcon,
85 AutoGeneratedGameIcon,
86 ForceAutoGeneratedGameIcon,
87 Look,
88 CreatorContextAsset,
89 Screenshot,
90}
91
92#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
93#[serde(rename_all = "camelCase")]
94pub struct ThumbnailResponse {
95 #[serde(rename = "targetId")]
96 pub id: u64,
97 pub image_url: String,
98 pub version: ThumbnailVersion,
99 pub state: ThumbnailState,
100}
101
102#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
103#[serde(rename_all = "camelCase")]
104pub struct ThumbnailResponseFromBatch {
105 #[serde(rename = "targetId")]
106 pub id: u64,
107 pub request_id: String,
108 pub image_url: String,
109 pub version: ThumbnailVersion,
110 pub state: ThumbnailState,
111 #[serde(rename = "errorMessage")]
112 pub error: String,
113 pub error_code: i32,
114}
115
116#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase")]
118pub struct ThumbnailBatchRequest<'a> {
119 #[serde(rename = "targetId")]
120 pub id: u64,
121 pub request_id: &'a str,
122 pub token: &'a str,
123 pub alias: &'a str,
124 #[serde(rename = "type")]
125 pub kind: ThumbnailRequestType,
126 pub size: ThumbnailSize,
127 pub format: ThumbnailFormat,
128 #[serde(rename = "isCircular")]
129 pub circular: bool,
130}
131
132impl Serialize for ThumbnailSize {
133 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134 where
135 S: Serializer,
136 {
137 serializer.serialize_str(&self.to_string())
138 }
139}
140
141impl std::fmt::Display for ThumbnailSize {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 let content = format!("{:?}", self);
144 write!(f, "{}", content.strip_prefix('S').unwrap())
145 }
146}
147
148impl TryFrom<&str> for ThumbnailSize {
149 type Error = &'static str;
150
151 fn try_from(value: &str) -> Result<Self, Self::Error> {
152 for size in ThumbnailSize::iter() {
153 if size.to_string().as_str() == value {
154 return Ok(size);
155 }
156 }
157
158 Err("Failed to convert string to ThumbnailSize")
159 }
160}
161
162impl ThumbnailFormat {
163 pub fn extension(&self) -> &str {
164 match self {
165 ThumbnailFormat::Png => "png",
166 ThumbnailFormat::Jpeg => "jpeg",
167 ThumbnailFormat::Webp => "webp",
168 }
169 }
170}
171
172async fn generic_thumbnail_api(
173 client: &mut Client,
174 ids: &[u64],
175 asset_name: &str,
176 domain: &str,
177 size: ThumbnailSize,
178 format: ThumbnailFormat,
179 circular: bool,
180 return_policy: Option<ReturnPolicy>,
181 count_per_universe: Option<u32>,
182 defaults: Option<bool>,
183) -> Result<Vec<ThumbnailResponse>, Error> {
184 let ids = ids
185 .iter()
186 .map(|x| x.to_string())
187 .collect::<Vec<String>>()
188 .join(",");
189
190 let asset_ids_key = format!("{asset_name}Ids");
191 let mut query = vec![
192 (asset_ids_key.as_str(), ids),
193 ("size", size.to_string()),
194 ("format", format.to_string()),
195 ("isCircular", circular.to_string()),
196 ];
197
198 if let Some(return_policy) = return_policy {
199 query.push(("returnPolicy", return_policy.to_string()));
200 }
201
202 if let Some(count_per_universe) = count_per_universe {
203 query.push(("countPerUniverse", count_per_universe.to_string()));
204 }
205
206 if let Some(defaults) = defaults {
207 query.push(("defaults", defaults.to_string()));
208 }
209
210 let result = client
211 .requestor
212 .client
213 .get(format!("{URL}/{domain}"))
214 .query(&query)
215 .headers(client.requestor.default_headers.clone())
216 .send()
217 .await;
218
219 #[derive(Clone, Debug, Deserialize)]
220 struct Response {
221 #[serde(rename = "data")]
222 thumbnails: Vec<ThumbnailResponse>,
223 }
224
225 let response = client.requestor.validate_response(result).await?;
226 Ok(client
227 .requestor
228 .parse_json::<Response>(response)
229 .await?
230 .thumbnails)
231}
232
233pub async fn assets(
234 client: &mut Client,
235 ids: &[u64],
236 size: ThumbnailSize,
237 format: ThumbnailFormat,
238 return_policy: ReturnPolicy,
239 circular: bool,
240) -> Result<Vec<ThumbnailResponse>, Error> {
241 generic_thumbnail_api(
242 client,
243 ids,
244 "asset",
245 "assets",
246 size,
247 format,
248 circular,
249 Some(return_policy),
250 None,
251 None,
252 )
253 .await
254}
255
256pub async fn asset_3d(
257 client: &mut Client,
258 id: u64,
259 encode_gltf: bool,
260) -> Result<ThumbnailResponse, Error> {
261 let result = client
262 .requestor
263 .client
264 .get(format!("{URL}/assets-thumbnail-3d"))
265 .query(&[
266 ("assetId", id.to_string()),
267 ("useGltf", encode_gltf.to_string()),
268 ])
269 .headers(client.requestor.default_headers.clone())
270 .send()
271 .await;
272
273 let response = client.requestor.validate_response(result).await?;
274 client
275 .requestor
276 .parse_json::<ThumbnailResponse>(response)
277 .await
278}
279
280pub async fn badge_icons(
281 client: &mut Client,
282 ids: &[u64],
283 size: ThumbnailSize,
284 format: ThumbnailFormat,
285 circular: bool,
286) -> Result<Vec<ThumbnailResponse>, Error> {
287 generic_thumbnail_api(
288 client,
289 ids,
290 "badge",
291 "badges/icons",
292 size,
293 format,
294 circular,
295 None,
296 None,
297 None,
298 )
299 .await
300}
301
302pub async fn bundles(
303 client: &mut Client,
304 ids: &[u64],
305 size: ThumbnailSize,
306 format: ThumbnailFormat,
307 circular: bool,
308) -> Result<Vec<ThumbnailResponse>, Error> {
309 generic_thumbnail_api(
310 client,
311 ids,
312 "bundle",
313 "bundles/thumbnails",
314 size,
315 format,
316 circular,
317 None,
318 None,
319 None,
320 )
321 .await
322}
323
324pub async fn developer_prodcuts(
325 client: &mut Client,
326 ids: &[u64],
327 size: ThumbnailSize,
328 format: ThumbnailFormat,
329 circular: bool,
330) -> Result<Vec<ThumbnailResponse>, Error> {
331 generic_thumbnail_api(
332 client,
333 ids,
334 "developerProduct",
335 "developer-products/icons",
336 size,
337 format,
338 circular,
339 None,
340 None,
341 None,
342 )
343 .await
344}
345
346pub async fn gamepasses(
347 client: &mut Client,
348 ids: &[u64],
349 size: ThumbnailSize,
350 format: ThumbnailFormat,
351 circular: bool,
352) -> Result<Vec<ThumbnailResponse>, Error> {
353 generic_thumbnail_api(
354 client,
355 ids,
356 "gamePass",
357 "game-passes",
358 size,
359 format,
360 circular,
361 None,
362 None,
363 None,
364 )
365 .await
366}
367
368pub async fn universe_thumbnails(
371 client: &mut Client,
372 universe_id: u64,
373 ids: &[u64],
374 size: ThumbnailSize,
375 format: ThumbnailFormat,
376 return_policy: ReturnPolicy,
377 circular: bool,
378) -> Result<Vec<ThumbnailResponse>, Error> {
379 generic_thumbnail_api(
380 client,
381 ids,
382 "thumbnail",
383 &format!("games/{universe_id}/thumbnails"),
384 size,
385 format,
386 circular,
387 Some(return_policy),
388 None,
389 None,
390 )
391 .await
392}
393
394pub async fn games(
395 client: &mut Client,
396 ids: &[u64],
397 size: ThumbnailSize,
398 format: ThumbnailFormat,
399 return_policy: ReturnPolicy,
400 circular: bool,
401 defaults: bool, count_per_universe: u32, ) -> Result<Vec<ThumbnailResponse>, Error> {
404 generic_thumbnail_api(
405 client,
406 ids,
407 "universe",
408 "games/multiget/thumbnails",
409 size,
410 format,
411 circular,
412 Some(return_policy),
413 Some(count_per_universe),
414 Some(defaults),
415 )
416 .await
417}
418
419pub async fn game_icons(
420 client: &mut Client,
421 ids: &[u64],
422 size: ThumbnailSize,
423 format: ThumbnailFormat,
424 return_policy: ReturnPolicy,
425 circular: bool,
426) -> Result<Vec<ThumbnailResponse>, Error> {
427 generic_thumbnail_api(
428 client,
429 ids,
430 "universe",
431 "games/icons",
432 size,
433 format,
434 circular,
435 Some(return_policy),
436 None,
437 None,
438 )
439 .await
440}
441
442pub async fn group_icons(
443 client: &mut Client,
444 ids: &[u64],
445 size: ThumbnailSize,
446 format: ThumbnailFormat,
447 circular: bool,
448) -> Result<Vec<ThumbnailResponse>, Error> {
449 generic_thumbnail_api(
450 client,
451 ids,
452 "group",
453 "groups/icons",
454 size,
455 format,
456 circular,
457 None,
458 None,
459 None,
460 )
461 .await
462}
463
464pub async fn place_icons(
465 client: &mut Client,
466 ids: &[u64],
467 size: ThumbnailSize,
468 format: ThumbnailFormat,
469 return_policy: ReturnPolicy,
470 circular: bool,
471) -> Result<Vec<ThumbnailResponse>, Error> {
472 generic_thumbnail_api(
473 client,
474 ids,
475 "place",
476 "places/gameicons",
477 size,
478 format,
479 circular,
480 Some(return_policy),
481 None,
482 None,
483 )
484 .await
485}
486
487pub async fn avatars(
488 client: &mut Client,
489 ids: &[u64],
490 size: ThumbnailSize,
491 format: ThumbnailFormat,
492 circular: bool,
493) -> Result<Vec<ThumbnailResponse>, Error> {
494 generic_thumbnail_api(
495 client,
496 ids,
497 "user",
498 "users/avatar",
499 size,
500 format,
501 circular,
502 None,
503 None,
504 None,
505 )
506 .await
507}
508
509pub async fn avatar_3d(client: &mut Client, id: u64) -> Result<ThumbnailResponse, Error> {
510 let result = client
511 .requestor
512 .client
513 .get(format!("{URL}/avatar-3d"))
514 .query(&[("userId", id)])
515 .headers(client.requestor.default_headers.clone())
516 .send()
517 .await;
518
519 let response = client.requestor.validate_response(result).await?;
520 client
521 .requestor
522 .parse_json::<ThumbnailResponse>(response)
523 .await
524}
525
526pub async fn avatar_busts(
527 client: &mut Client,
528 ids: &[u64],
529 size: ThumbnailSize,
530 format: ThumbnailFormat,
531 circular: bool,
532) -> Result<Vec<ThumbnailResponse>, Error> {
533 generic_thumbnail_api(
534 client,
535 ids,
536 "user",
537 "users/avatar-bust",
538 size,
539 format,
540 circular,
541 None,
542 None,
543 None,
544 )
545 .await
546}
547
548pub async fn avatar_headshots(
549 client: &mut Client,
550 ids: &[u64],
551 size: ThumbnailSize,
552 format: ThumbnailFormat,
553 circular: bool,
554) -> Result<Vec<ThumbnailResponse>, Error> {
555 generic_thumbnail_api(
556 client,
557 ids,
558 "user",
559 "users/avatar-headshot",
560 size,
561 format,
562 circular,
563 None,
564 None,
565 None,
566 )
567 .await
568}
569
570pub async fn outfit_3d(client: &mut Client, id: u64) -> Result<ThumbnailResponse, Error> {
571 let result = client
572 .requestor
573 .client
574 .get(format!("{URL}/outfit-3d"))
575 .query(&[("outfitId", id)])
576 .headers(client.requestor.default_headers.clone())
577 .send()
578 .await;
579
580 let response = client.requestor.validate_response(result).await?;
581 client
582 .requestor
583 .parse_json::<ThumbnailResponse>(response)
584 .await
585}
586
587pub async fn outfits(
588 client: &mut Client,
589 ids: &[u64],
590 size: ThumbnailSize,
591 format: ThumbnailFormat,
592 circular: bool,
593) -> Result<Vec<ThumbnailResponse>, Error> {
594 generic_thumbnail_api(
595 client,
596 ids,
597 "userOutfit",
598 "users/outfits",
599 size,
600 format,
601 circular,
602 None,
603 None,
604 None,
605 )
606 .await
607}
608
609pub async fn batch(
610 client: &mut Client,
611 requests: Vec<ThumbnailBatchRequest<'_>>,
612) -> Result<Vec<ThumbnailResponseFromBatch>, Error> {
613 let result = client
614 .requestor
615 .client
616 .post(format!("{URL}/batch"))
617 .headers(client.requestor.default_headers.clone())
618 .json(&requests)
619 .send()
620 .await;
621
622 #[derive(Clone, Debug, Deserialize)]
623 struct Response {
624 #[serde(rename = "data")]
625 thumbnails: Vec<ThumbnailResponseFromBatch>,
626 }
627
628 let response = client.requestor.validate_response(result).await?;
629 Ok(client
630 .requestor
631 .parse_json::<Response>(response)
632 .await?
633 .thumbnails)
634}
635
636