scratch_io/
itch_api_types.rs

1use serde::{Serialize, Deserialize};
2use time::{OffsetDateTime, serde::rfc3339, format_description::well_known::Rfc3339};
3use std::fmt;
4
5const ITCH_API_V1_BASE_URL: &str = "https://itch.io/api/1";
6const ITCH_API_V2_BASE_URL: &str = "https://api.itch.io";
7
8pub fn empty_object_as_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error> where
9  D: serde::de::Deserializer<'de>,
10  T: Deserialize<'de>,
11{
12  struct Helper<T>(std::marker::PhantomData<T>);
13
14  impl<'de, T> serde::de::Visitor<'de> for Helper<T> where
15    T: Deserialize<'de>,
16  {
17    type Value = Vec<T>;
18
19    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
20      formatter.write_str("an array or an empty object")
21    }
22
23    fn visit_seq<A>(self, mut seq: A) -> Result<Vec<T>, A::Error> where
24      A: serde::de::SeqAccess<'de>,
25    {
26      let mut items = Vec::new();
27      while let Some(item) = seq.next_element()? {
28        items.push(item);
29      }
30      Ok(items)
31    }
32
33    fn visit_map<A>(self, mut map: A) -> Result<Vec<T>, A::Error> where
34      A: serde::de::MapAccess<'de>,
35    {
36      // Consume all keys without using them, returning empty Vec
37      while let Some((_k, _v)) = map.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()? {
38        // Just ignore
39      }
40      Ok(vec![])
41    }
42  }
43
44  deserializer.deserialize_any(Helper(std::marker::PhantomData))
45}
46
47/// A itch.io API address
48/// 
49/// Use the Other variant with the full URL when it isn't a known API version
50pub enum ItchApiUrl<'a> {
51  V1(&'a str),
52  V2(&'a str),
53  Other(&'a str),
54}
55
56impl fmt::Display for ItchApiUrl<'_> {
57  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58    write!(f, "{}", 
59      match self {
60        ItchApiUrl::V1(u) => format!("{ITCH_API_V1_BASE_URL}/{u}"),
61        ItchApiUrl::V2(u) => format!("{ITCH_API_V2_BASE_URL}/{u}"),
62        ItchApiUrl::Other(u) => format!("{u}"),
63      }
64    )
65  }
66}
67
68#[derive(Serialize, Deserialize)]
69pub enum GameTrait {
70  #[serde(rename = "p_linux")]
71  PLinux,
72  #[serde(rename = "p_windows")]
73  PWindows,
74  #[serde(rename = "p_osx")]
75  POSX,
76  #[serde(rename = "p_android")]
77  PAndroid,
78  #[serde(rename = "can_be_bought")]
79  CanBeBought,
80  #[serde(rename = "has_demo")]
81  HasDemo,
82  #[serde(rename = "in_press_system")]
83  InPressSystem,
84}
85
86impl fmt::Display for GameTrait {
87  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88    write!(f, "{}", serde_json::to_string(&self).unwrap())
89  }
90}
91
92#[derive(Serialize, Deserialize)]
93pub enum UploadTrait {
94  #[serde(rename = "p_linux")]
95  PLinux,
96  #[serde(rename = "p_windows")]
97  PWindows,
98  #[serde(rename = "p_osx")]
99  POSX,
100  #[serde(rename = "p_android")]
101  PAndroid,
102  #[serde(rename = "demo")]
103  Demo,
104}
105
106impl fmt::Display for UploadTrait {
107  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108    write!(f, "{}", serde_json::to_string(&self).unwrap())
109  }
110}
111
112#[derive(Serialize, Deserialize)]
113pub enum GameClassification {
114  #[serde(rename = "game")]
115  Game,
116  #[serde(rename = "assets")]
117  Assets,
118  #[serde(rename = "game_mod")]
119  GameMod,
120  #[serde(rename = "physical_game")]
121  PhysicalGame,
122  #[serde(rename = "soundtrack")]
123  Soundtrack,
124  #[serde(rename = "tool")]
125  Tool,
126  #[serde(rename = "comic")]
127  Comic,
128  #[serde(rename = "book")]
129  Book,
130  #[serde(rename = "other")]
131  Other,
132}
133
134impl fmt::Display for GameClassification {
135  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136    write!(f, "{}", serde_json::to_string(&self).unwrap())
137  }
138}
139
140#[derive(Serialize, Deserialize)]
141pub enum GameType {
142  #[serde(rename = "default")]
143  Default,
144  #[serde(rename = "html")]
145  HTML,
146  #[serde(rename = "flash")]
147  Flash,
148  #[serde(rename = "java")]
149  Java,
150  #[serde(rename = "unity")]
151  Unity,
152}
153
154impl fmt::Display for GameType {
155  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156    write!(f, "{}", serde_json::to_string(&self).unwrap())
157  }
158}
159
160#[derive(Serialize, Deserialize)]
161pub enum UploadType {
162  #[serde(rename = "default")]
163  Default,
164  #[serde(rename = "html")]
165  HTML,
166  #[serde(rename = "flash")]
167  Flash,
168  #[serde(rename = "java")]
169  Java,
170  #[serde(rename = "unity")]
171  Unity,
172  #[serde(rename = "soundtrack")]
173  Soundtrack,
174  #[serde(rename = "book")]
175  Book,
176  #[serde(rename = "video")]
177  Video,
178  #[serde(rename = "documentation")]
179  Documentation,
180  #[serde(rename = "mod")]
181  Mod,
182  #[serde(rename = "audio_assets")]
183  AudioAssets,
184  #[serde(rename = "graphical_assets")]
185  GraphicalAssets,
186  #[serde(rename = "sourcecode")]
187  Sourcecode,
188  #[serde(rename = "other")]
189  Other,
190}
191
192impl fmt::Display for UploadType {
193  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194    write!(f, "{}", serde_json::to_string(&self).unwrap())
195  }
196}
197
198#[derive(Serialize, Deserialize)]
199pub struct User {
200  pub id: u64,
201  pub username: String,
202  pub display_name: Option<String>,
203  pub url: String,
204  pub cover_url: Option<String>,
205  pub still_cover_url: Option<String>,
206  pub press_user: Option<bool>,
207  pub developer: Option<bool>,
208  pub gamer: Option<bool>,
209}
210
211impl User {
212  pub fn get_name(&self) -> &str {
213    self.display_name.as_deref().unwrap_or(self.username.as_str())
214  }
215}
216
217impl fmt::Display for User {
218  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219    write!(f, "\
220Id: {}
221Name: {}
222Display name: {}
223URL: {}
224Cover URL: {}",
225      self.id,
226      self.username,
227      self.display_name.as_deref().unwrap_or_default(),
228      self.url,
229      self.cover_url.as_deref().unwrap_or_default(),
230    )
231  }
232}
233
234#[derive(Serialize, Deserialize)]
235pub struct Game {
236  pub id: u64,
237  pub url: String,
238  pub title: String,
239  pub short_text: Option<String>,
240  pub r#type: GameType,
241  pub classification: GameClassification,
242  pub cover_url: Option<String>,
243  #[serde(with = "rfc3339")]
244  pub created_at: OffsetDateTime,
245  #[serde(with = "rfc3339::option", default)]
246  pub published_at: Option<OffsetDateTime>,
247  pub min_price: u64,
248  pub user: User,
249  #[serde(deserialize_with = "empty_object_as_vec")]
250  pub traits: Vec<GameTrait>,
251}
252
253impl fmt::Display for Game {
254  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255    write!(f, "\
256Id: {}
257Game: {}
258  Description: {}
259  URL: {}
260  Cover URL: {}
261  Author: {}
262  Price: {}
263  Classification: {}
264  Type: {}
265  Created at: {}
266  Published at: {}
267  Traits: {}",
268      self.id,
269      self.title,
270      self.short_text.as_deref().unwrap_or_default(),
271      self.url,
272      self.cover_url.as_deref().unwrap_or_default(),
273      self.user.get_name(),
274      if self.min_price <= 0 { "Free" } else { "Paid" },
275      self.classification,
276      self.r#type,
277      self.created_at.format(&Rfc3339).unwrap_or_default(),
278      self.published_at.as_ref().and_then(|date| date.format(&Rfc3339).ok()).unwrap_or_default(),
279      self.traits.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(", ")
280    )
281  }
282}
283
284#[derive(Serialize, Deserialize)]
285pub struct Upload {
286  pub position: u64,
287  pub id: u64,
288  pub game_id: u64,
289  pub size: Option<u64>,
290  pub r#type: UploadType,
291  #[serde(deserialize_with = "empty_object_as_vec")]
292  pub traits: Vec<UploadTrait>,
293  pub filename: String,
294  pub display_name: Option<String>,
295  pub storage: String,
296  pub host: Option<String>,
297  #[serde(with = "rfc3339")]
298  pub created_at: OffsetDateTime,
299  #[serde(with = "rfc3339")]
300  pub updated_at: OffsetDateTime,
301  pub md5_hash: Option<String>,
302}
303
304impl fmt::Display for Upload {
305  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306    write!(f, 
307"    Position: {}
308    ID: {}
309      Size: {}
310      Type: {}
311      Filename: {}
312      Display name: {}
313      Storage: {}
314      Created at: {}
315      Updated at: {}
316      MD5 hash: {}
317      Traits: {}",
318      self.position,
319      self.id,
320      self.size.as_ref().map(|n| n.to_string()).unwrap_or_default(),
321      self.r#type,
322      self.filename,
323      self.display_name.as_deref().unwrap_or_default(),
324      self.storage,
325      self.created_at.format(&Rfc3339).unwrap_or_default(),
326      self.updated_at.format(&Rfc3339).unwrap_or_default(),
327      self.md5_hash.as_deref().unwrap_or_default(),
328      self.traits.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(", ")
329    )
330  }
331}
332
333#[derive(Deserialize)]
334pub struct Collection {
335  pub id: u64,
336  pub title: String,
337  pub games_count: u64,
338  #[serde(with = "rfc3339")]
339  pub created_at: OffsetDateTime,
340  #[serde(with = "rfc3339")]
341  pub updated_at: OffsetDateTime,
342}
343
344impl fmt::Display for Collection {
345  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346    write!(f, "\
347Id: {}
348Name: {}
349  Games count: {}
350  Created at: {}
351  Updated at: {}",
352      self.id,
353      self.title,
354      self.games_count,
355      self.created_at.format(&Rfc3339).unwrap_or_default(),
356      self.updated_at.format(&Rfc3339).unwrap_or_default(),
357    )
358  }
359}
360
361#[derive(Deserialize)]
362pub struct CollectionGameItem {
363  pub game: CollectionGame,
364  pub position: u64,
365  pub user_id: u64,
366  #[serde(with = "rfc3339")]
367  pub created_at: OffsetDateTime,
368}
369
370impl fmt::Display for CollectionGameItem {
371  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372    write!(f, "\
373Position: {}
374{}",
375      self.position,
376      self.game,
377    )
378  }
379}
380
381#[derive(Deserialize)]
382pub struct CollectionGame {
383  pub id: u64,
384  pub url: String,
385  pub title: String,
386  pub short_text: Option<String>,
387  pub r#type: GameType,
388  pub classification: GameClassification,
389  pub cover_url: Option<String>,
390  #[serde(with = "rfc3339")]
391  pub created_at: OffsetDateTime,
392  #[serde(with = "rfc3339::option", default)]
393  pub published_at: Option<OffsetDateTime>,
394  pub min_price: u64,
395  #[serde(deserialize_with = "empty_object_as_vec")]
396  pub traits: Vec<GameTrait>,
397}
398
399impl fmt::Display for CollectionGame {
400  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401    write!(f, "\
402Id: {}
403Game: {}
404  Description: {}
405  URL: {}
406  Cover URL: {}
407  Price: {}
408  Classification: {}
409  Type: {}
410  Created at: {}
411  Published at: {}
412  Traits: {}",
413      self.id,
414      self.title,
415      self.short_text.as_deref().unwrap_or_default(),
416      self.url,
417      self.cover_url.as_deref().unwrap_or_default(),
418      if self.min_price <= 0 { "Free" } else { "Paid" },
419      self.classification,
420      self.r#type,
421      self.created_at.format(&Rfc3339).unwrap_or_default(),
422      self.published_at.as_ref().and_then(|date| date.format(&Rfc3339).ok()).unwrap_or_default(),
423      self.traits.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(", ")
424    )
425  }
426}
427
428#[derive(Deserialize)]
429pub struct OwnedKey {
430  pub id: u64,
431  pub game_id: u64,
432  pub downloads: u64,
433  pub game: Game,
434  #[serde(with = "rfc3339")]
435  pub created_at: OffsetDateTime,
436  #[serde(with = "rfc3339")]
437  pub updated_at: OffsetDateTime,
438}
439
440impl fmt::Display for OwnedKey {
441  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442    write!(f, "\
443Id: {}
444  Game Id: {}
445  Downloads: {}
446  Created at: {}
447  Updated at: {}",
448      self.id,
449      self.game_id,
450      self.downloads,
451      self.created_at.format(&Rfc3339).unwrap_or_default(),
452      self.updated_at.format(&Rfc3339).unwrap_or_default(),
453    )
454  }
455}
456
457#[derive(Deserialize)]
458pub struct ItchCookie {
459  pub itchio: String,
460}
461
462#[derive(Deserialize)]
463pub struct ItchKey {
464  pub key: String,
465  pub id: u64,
466  pub user_id: u64,
467  pub source: String,
468  pub revoked: Option<bool>,
469  #[serde(with = "rfc3339")]
470  pub created_at: OffsetDateTime,
471  #[serde(with = "rfc3339")]
472  pub updated_at: OffsetDateTime,
473}
474
475#[derive(Deserialize)]
476#[serde(untagged)]
477pub enum ApiResponse<T> {
478  Success(T),
479  Error {
480    #[serde(deserialize_with = "empty_object_as_vec")]
481    errors: Vec<String>,
482  },
483}
484
485impl<T> ApiResponse<T> {
486  pub fn into_result(self) -> Result<T, String> {
487    match self {
488      ApiResponse::Success(data) => Ok(data),
489      ApiResponse::Error { errors } => Err(format!("The server replied with an error:\n{}", errors.join("\n"))),
490    }
491  }
492}
493
494#[derive(Deserialize)]
495#[serde(untagged)]
496pub enum LoginResponse {
497  Success(LoginSuccess),
498  CaptchaError(LoginCaptchaError),
499  TOTPError(LoginTOTPError),
500}
501
502#[derive(Deserialize)]
503pub struct LoginSuccess {
504  pub success: bool,
505  pub cookie: ItchCookie,
506  pub key: ItchKey,
507}
508
509#[derive(Deserialize)]
510pub struct LoginCaptchaError {
511  pub success: bool,
512  pub recaptcha_needed: bool,
513  pub recaptcha_url: String,
514}
515
516#[derive(Deserialize)]
517pub struct LoginTOTPError {
518  pub success: bool,
519  pub totp_needed: bool,
520  pub token: String,
521}
522
523#[derive(Deserialize)]
524pub struct ProfileResponse {
525  pub user: User,
526}
527
528#[derive(Deserialize)]
529pub struct GameInfoResponse {
530  pub game: Game,
531}
532
533#[derive(Deserialize)]
534pub struct GameUploadsResponse {
535  #[serde(deserialize_with = "empty_object_as_vec")]
536  pub uploads: Vec<Upload>,
537}
538
539#[derive(Deserialize)]
540pub struct UploadResponse {
541  pub upload: Upload,
542}
543
544#[derive(Deserialize)]
545pub struct CollectionsResponse {
546  #[serde(deserialize_with = "empty_object_as_vec")]
547  pub collections: Vec<Collection>,
548}
549
550#[derive(Deserialize)]
551pub struct CollectionGamesResponse {
552  pub page: u64,
553  pub per_page: u64,
554  #[serde(deserialize_with = "empty_object_as_vec")]
555  pub collection_games: Vec<CollectionGameItem>,
556}
557
558#[derive(Deserialize)]
559pub struct OwnedKeysResponse {
560  pub page: u64,
561  pub per_page: u64,
562  #[serde(deserialize_with = "empty_object_as_vec")]
563  pub owned_keys: Vec<OwnedKey>,
564}