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