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 while let Some((_k, _v)) = map.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()? {
38 }
40 Ok(vec![])
41 }
42 }
43
44 deserializer.deserialize_any(Helper(std::marker::PhantomData))
45}
46
47pub 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}