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