1use serde::{Deserialize, Serialize, Serializer};
2use strum::{EnumIter, IntoEnumIterator};
3use strum_macros::{Display, EnumString};
4
5use crate::endpoint;
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 ThumbnailRequestType {
66 Avatar = 1,
67 AvatarHeadShot,
68 GameIcon,
69 BadgeIcon,
70 GameThumbnail,
71 GamePass,
72 Asset,
73 BundleThumbnail,
74 Outfit,
75 GroupIcon,
76 DeveloperProduct,
77 AutoGeneratedAsset,
78 AvatarBust,
79 PlaceIcon,
80 AutoGeneratedGameIcon,
81 ForceAutoGeneratedGameIcon,
82 Look,
83 CreatorContextAsset,
84 Screenshot,
85}
86
87#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
88#[serde(rename_all = "camelCase")]
89pub struct ThumbnailResponse {
90 #[serde(rename = "targetId")]
91 pub id: u64,
92 pub image_url: String,
93 pub version: String,
94 pub state: ThumbnailState,
95}
96
97#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
98#[serde(rename_all = "camelCase")]
99pub struct ThumbnailResponseFromBatch {
100 #[serde(rename = "targetId")]
101 pub id: u64,
102 pub request_id: String,
103 pub image_url: String,
104 pub version: String,
105 pub state: ThumbnailState,
106 #[serde(rename = "errorMessage")]
107 pub error: String,
108 pub error_code: i32,
109}
110
111#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct ThumbnailBatchRequest<'a> {
114 #[serde(rename = "targetId")]
115 pub id: u64,
116 pub request_id: &'a str,
117 pub token: &'a str,
118 pub alias: &'a str,
119 #[serde(rename = "type")]
120 pub kind: ThumbnailRequestType,
121 pub size: ThumbnailSize,
122 pub format: ThumbnailFormat,
123 #[serde(rename = "isCircular")]
124 pub circular: bool,
125}
126
127impl Serialize for ThumbnailSize {
128 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129 where
130 S: Serializer,
131 {
132 serializer.serialize_str(&self.to_string())
133 }
134}
135
136impl std::fmt::Display for ThumbnailSize {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 let content = format!("{:?}", self);
139 write!(f, "{}", content.strip_prefix('S').unwrap())
140 }
141}
142
143impl TryFrom<&str> for ThumbnailSize {
144 type Error = &'static str;
145
146 fn try_from(value: &str) -> Result<Self, Self::Error> {
147 for size in ThumbnailSize::iter() {
148 if size.to_string().as_str() == value {
149 return Ok(size);
150 }
151 }
152
153 Err("Failed to convert string to ThumbnailSize")
154 }
155}
156
157impl ThumbnailFormat {
158 pub fn extension(&self) -> &str {
159 match self {
160 ThumbnailFormat::Png => "png",
161 ThumbnailFormat::Jpeg => "jpeg",
162 ThumbnailFormat::Webp => "webp",
163 }
164 }
165}
166
167endpoint! {
168 assets(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, return_policy: ReturnPolicy, circular: bool) -> Vec<ThumbnailResponse> {
170 GET "{URL}/assets";
171 types {
172 Response { data("data"): Vec<ThumbnailResponse> }
173 }
174 prelude {
175 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
176 let size = size.to_string();
177 let format = format.to_string();
178 let return_policy = return_policy.to_string();
179 let circular = circular.to_string();
180 }
181 query {
182 "assetIds" => &joined_ids,
183 "size" => &size,
184 "format" => &format,
185 "returnPolicy" => &return_policy,
186 "isCircular" => &circular,
187 }
188 map |r: Response| r.data
189 }
190
191 asset_3d(id: u64, encode_gltf: bool) -> ThumbnailResponse {
193 GET "{URL}/assets-thumbnail-3d";
194 prelude {
195 let id = id.to_string();
196 let encode_gltf = encode_gltf.to_string();
197 }
198 query {
199 "assetId" => &id,
200 "useGltf" => &encode_gltf,
201 }
202 }
203
204 badge_icons(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
206 GET "{URL}/badges/icons";
207 types {
208 Response { data("data"): Vec<ThumbnailResponse> }
209 }
210 prelude {
211 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
212 let size = size.to_string();
213 let format = format.to_string();
214 let circular = circular.to_string();
215 }
216 query {
217 "badgeIds" => &joined_ids,
218 "size" => &size,
219 "format" => &format,
220 "isCircular" => &circular,
221 }
222 map |r: Response| r.data
223 }
224
225 bundles(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
227 GET "{URL}/bundles/thumbnails";
228 types {
229 Response { data("data"): Vec<ThumbnailResponse> }
230 }
231 prelude {
232 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
233 let size = size.to_string();
234 let format = format.to_string();
235 let circular = circular.to_string();
236 }
237 query {
238 "bundleIds" => &joined_ids,
239 "size" => &size,
240 "format" => &format,
241 "isCircular" => &circular,
242 }
243 map |r: Response| r.data
244 }
245
246 developer_products(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
248 GET "{URL}/developer-products/icons";
249 types {
250 Response { data("data"): Vec<ThumbnailResponse> }
251 }
252 prelude {
253 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
254 let size = size.to_string();
255 let format = format.to_string();
256 let circular = circular.to_string();
257 }
258 query {
259 "developerProductIds" => &joined_ids,
260 "size" => &size,
261 "format" => &format,
262 "isCircular" => &circular,
263 }
264 map |r: Response| r.data
265 }
266
267 gamepasses(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
269 GET "{URL}/game-passes";
270 types {
271 Response { data("data"): Vec<ThumbnailResponse> }
272 }
273 prelude {
274 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
275 let size = size.to_string();
276 let format = format.to_string();
277 let circular = circular.to_string();
278 }
279 query {
280 "gamePassIds" => &joined_ids,
281 "size" => &size,
282 "format" => &format,
283 "isCircular" => &circular,
284 }
285 map |r: Response| r.data
286 }
287
288 universe_thumbnails(universe_id: u64, ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, return_policy: ReturnPolicy, circular: bool) -> Vec<ThumbnailResponse> {
291 GET "{URL}/games/{universe_id}/thumbnails";
292 types {
293 Response { data("data"): Vec<ThumbnailResponse> }
294 }
295 prelude {
296 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
297 let size = size.to_string();
298 let format = format.to_string();
299 let return_policy = return_policy.to_string();
300 let circular = circular.to_string();
301 }
302 query {
303 "thumbnailIds" => &joined_ids,
304 "size" => &size,
305 "format" => &format,
306 "returnPolicy" => &return_policy,
307 "isCircular" => &circular,
308 }
309 map |r: Response| r.data
310 }
311
312 games(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, return_policy: ReturnPolicy, circular: bool, defaults: bool, count_per_universe: u32) -> Vec<ThumbnailResponse> {
314 GET "{URL}/games/multiget/thumbnails";
315 types {
316 Response { data("data"): Vec<ThumbnailResponse> }
317 }
318 prelude {
319 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
320 let size = size.to_string();
321 let format = format.to_string();
322 let return_policy = return_policy.to_string();
323 let circular = circular.to_string();
324 let defaults = defaults.to_string();
325 let count_per_universe = count_per_universe.to_string();
326 }
327 query {
328 "universeIds" => &joined_ids,
329 "size" => &size,
330 "format" => &format,
331 "returnPolicy" => &return_policy,
332 "isCircular" => &circular,
333 "defaults" => &defaults,
334 "countPerUniverse" => &count_per_universe,
335 }
336 map |r: Response| r.data
337 }
338
339 game_icons(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, return_policy: ReturnPolicy, circular: bool) -> Vec<ThumbnailResponse> {
341 GET "{URL}/games/icons";
342 types {
343 Response { data("data"): Vec<ThumbnailResponse> }
344 }
345 prelude {
346 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
347 let size = size.to_string();
348 let format = format.to_string();
349 let return_policy = return_policy.to_string();
350 let circular = circular.to_string();
351 }
352 query {
353 "universeIds" => &joined_ids,
354 "size" => &size,
355 "format" => &format,
356 "returnPolicy" => &return_policy,
357 "isCircular" => &circular,
358 }
359 map |r: Response| r.data
360 }
361
362 group_icons(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
364 GET "{URL}/groups/icons";
365 types {
366 Response { data("data"): Vec<ThumbnailResponse> }
367 }
368 prelude {
369 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
370 let size = size.to_string();
371 let format = format.to_string();
372 let circular = circular.to_string();
373 }
374 query {
375 "groupIds" => &joined_ids,
376 "size" => &size,
377 "format" => &format,
378 "isCircular" => &circular,
379 }
380 map |r: Response| r.data
381 }
382
383 place_icons(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, return_policy: ReturnPolicy, circular: bool) -> Vec<ThumbnailResponse> {
385 GET "{URL}/places/gameicons";
386 types {
387 Response { data("data"): Vec<ThumbnailResponse> }
388 }
389 prelude {
390 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
391 let size = size.to_string();
392 let format = format.to_string();
393 let return_policy = return_policy.to_string();
394 let circular = circular.to_string();
395 }
396 query {
397 "placeIds" => &joined_ids,
398 "size" => &size,
399 "format" => &format,
400 "returnPolicy" => &return_policy,
401 "isCircular" => &circular,
402 }
403 map |r: Response| r.data
404 }
405
406 avatars(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
408 GET "{URL}/users/avatar";
409 types {
410 Response { data("data"): Vec<ThumbnailResponse> }
411 }
412 prelude {
413 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
414 let size = size.to_string();
415 let format = format.to_string();
416 let circular = circular.to_string();
417 }
418 query {
419 "userIds" => &joined_ids,
420 "size" => &size,
421 "format" => &format,
422 "isCircular" => &circular,
423 }
424 map |r: Response| r.data
425 }
426
427 avatar_3d(id: u64) -> ThumbnailResponse {
429 GET "{URL}/avatar-3d";
430 prelude {
431 let id = id.to_string();
432 }
433 query {
434 "userId" => &id,
435 }
436 }
437
438 avatar_busts(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
440 GET "{URL}/users/avatar-bust";
441 types {
442 Response { data("data"): Vec<ThumbnailResponse> }
443 }
444 prelude {
445 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
446 let size = size.to_string();
447 let format = format.to_string();
448 let circular = circular.to_string();
449 }
450 query {
451 "userIds" => &joined_ids,
452 "size" => &size,
453 "format" => &format,
454 "isCircular" => &circular,
455 }
456 map |r: Response| r.data
457 }
458
459 avatar_headshots(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
461 GET "{URL}/users/avatar-headshot";
462 types {
463 Response { data("data"): Vec<ThumbnailResponse> }
464 }
465 prelude {
466 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
467 let size = size.to_string();
468 let format = format.to_string();
469 let circular = circular.to_string();
470 }
471 query {
472 "userIds" => &joined_ids,
473 "size" => &size,
474 "format" => &format,
475 "isCircular" => &circular,
476 }
477 map |r: Response| r.data
478 }
479
480 outfit_3d(id: u64) -> ThumbnailResponse {
482 GET "{URL}/outfit-3d";
483 prelude {
484 let id = id.to_string();
485 }
486 query {
487 "outfitId" => &id,
488 }
489 }
490
491 outfits(ids: &[u64], size: ThumbnailSize, format: ThumbnailFormat, circular: bool) -> Vec<ThumbnailResponse> {
493 GET "{URL}/users/outfits";
494 types {
495 Response { data("data"): Vec<ThumbnailResponse> }
496 }
497 prelude {
498 let joined_ids = ids.iter().map(|x| x.to_string()).collect::<Vec<String>>().join(",");
499 let size = size.to_string();
500 let format = format.to_string();
501 let circular = circular.to_string();
502 }
503 query {
504 "userOutfitIds" => &joined_ids,
505 "size" => &size,
506 "format" => &format,
507 "isCircular" => &circular,
508 }
509 map |r: Response| r.data
510 }
511
512 batch(requests: Vec<ThumbnailBatchRequest<'_>>) -> Vec<ThumbnailResponseFromBatch> {
514 POST "{URL}/batch";
515 types {
516 Response { data("data"): Vec<ThumbnailResponseFromBatch> }
517 }
518 body_serialize { &requests }
519 map |r: Response| r.data
520 }
521}
522
523