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