1use serde::{Deserialize, Serialize};
2use serde_with::{DefaultOnError, serde_as};
3use thiserror::Error;
4use time::{OffsetDateTime, serde::rfc3339};
5
6const ITCH_API_V1_BASE_URL: &str = "https://itch.io/api/1/";
7const ITCH_API_V2_BASE_URL: &str = "https://api.itch.io/";
8
9pub type UserID = u64;
10pub type CollectionID = u64;
11pub type GameID = u64;
12pub type UploadID = u64;
13pub type BuildID = u64;
14pub type ItchKeyID = u64;
15pub type OwnedKeyID = u64;
16pub type SaleID = u64;
17
18pub(super) fn empty_object_as_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
30where
31 D: serde::de::Deserializer<'de>,
32 T: Deserialize<'de>,
33{
34 struct Helper<T>(std::marker::PhantomData<T>);
35
36 impl<'de, T> serde::de::Visitor<'de> for Helper<T>
37 where
38 T: Deserialize<'de>,
39 {
40 type Value = Vec<T>;
41
42 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
43 formatter.write_str("an array or an empty object")
44 }
45
46 fn visit_seq<A>(self, mut seq: A) -> Result<Vec<T>, A::Error>
47 where
48 A: serde::de::SeqAccess<'de>,
49 {
50 let mut items = Vec::new();
51 while let Some(item) = seq.next_element()? {
52 items.push(item);
53 }
54 Ok(items)
55 }
56
57 fn visit_map<A>(self, mut map: A) -> Result<Vec<T>, A::Error>
58 where
59 A: serde::de::MapAccess<'de>,
60 {
61 while let Some((_k, _v)) = map.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()? {
63 }
65 Ok(vec![])
66 }
67 }
68
69 deserializer.deserialize_any(Helper(std::marker::PhantomData))
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
82pub enum ItchApiVersion {
83 V1,
84 V2,
85 Other,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct ItchApiUrl {
93 version: ItchApiVersion,
94 url: String,
95}
96
97impl<'a> ItchApiUrl {
98 pub fn from_api_endpoint(
103 version: ItchApiVersion,
104 endpoint: impl Into<std::borrow::Cow<'a, str>>,
105 ) -> Self {
106 let endpoint = endpoint.into();
107 Self {
108 version,
109 url: match version {
110 ItchApiVersion::V1 => format!("{ITCH_API_V1_BASE_URL}{endpoint}"),
111 ItchApiVersion::V2 => format!("{ITCH_API_V2_BASE_URL}{endpoint}"),
112 ItchApiVersion::Other => endpoint.into_owned(),
113 },
114 }
115 }
116
117 #[must_use]
119 pub fn get_version(&self) -> ItchApiVersion {
120 self.version
121 }
122}
123
124impl ItchApiUrl {
125 #[must_use]
127 pub fn as_str(&self) -> &str {
128 &self.url
129 }
130}
131
132impl std::fmt::Display for ItchApiUrl {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "{}", self.url)
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct ItchCookie {
141 pub itchio: String,
142}
143
144#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum ItchKeySource {
147 Desktop,
148 Android,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct ItchKey {
153 pub key: String,
154 pub id: ItchKeyID,
155 pub user_id: UserID,
156 pub source: ItchKeySource,
157 pub revoked: Option<bool>,
158 #[serde(with = "rfc3339")]
159 pub created_at: OffsetDateTime,
160 #[serde(with = "rfc3339")]
161 pub updated_at: OffsetDateTime,
162 #[serde(with = "rfc3339::option", default)]
163 pub last_used_at: Option<OffsetDateTime>,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct LoginSuccess {
168 pub success: bool,
169 pub cookie: ItchCookie,
170 pub key: ItchKey,
171}
172
173#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
176#[error(
177 r#"A reCAPTCHA verification is required to continue!
178 Go to "{recaptcha_url}" and solve the reCAPTCHA.
179 To obtain the token, paste the following command on the developer console:
180 console.log(grecaptcha.getResponse())
181 Then run the login command again with the --recaptcha-response option."#
182)]
183pub struct LoginCaptchaError {
184 pub success: bool,
185 pub recaptcha_needed: bool,
186 pub recaptcha_url: String,
187}
188
189#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[error(
193 r#"The account has two-step verification enabled via TOTP.
194 To complete the login, run the totp verification command with the following options:
195 --totp-token="{token}"
196 --totp-code={{VERIFICATION_CODE}}"#
197)]
198pub struct LoginTOTPError {
199 pub success: bool,
200 pub totp_needed: bool,
201 pub token: String,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct User {
206 pub id: UserID,
207 pub username: String,
208 pub display_name: Option<String>,
209 pub url: String,
210 pub cover_url: Option<String>,
211 pub still_cover_url: Option<String>,
213}
214
215impl User {
216 #[must_use]
218 pub fn get_name(&self) -> &str {
219 self.display_name.as_deref().unwrap_or(&self.username)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct Profile {
225 #[serde(flatten)]
226 pub user: User,
227 pub gamer: bool,
228 pub developer: bool,
229 pub press_user: bool,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct GameSale {
234 pub id: SaleID,
235 pub rate: i8,
238 #[serde(with = "rfc3339")]
239 pub start_date: OffsetDateTime,
240 #[serde(with = "rfc3339")]
241 pub end_date: OffsetDateTime,
242}
243
244#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum GameType {
247 Default,
248 Html,
249 Flash,
250 Java,
251 Unity,
252}
253
254#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum GameClassification {
257 Game,
258 Assets,
259 GameMod,
260 PhysicalGame,
261 Soundtrack,
262 Tool,
263 Comic,
264 Book,
265 Other,
266}
267
268#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
269#[serde(rename_all = "snake_case")]
270pub enum GameTrait {
271 PLinux,
272 PWindows,
273 POsx,
274 PAndroid,
275 CanBeBought,
276 HasDemo,
277 InPressSystem,
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284pub struct GameCommon {
285 pub id: GameID,
286 pub url: String,
287 pub title: String,
288 pub short_text: Option<String>,
289 pub r#type: GameType,
290 pub classification: GameClassification,
291 pub cover_url: Option<String>,
292 #[serde(with = "rfc3339")]
293 pub created_at: OffsetDateTime,
294 #[serde(with = "rfc3339::option", default)]
295 pub published_at: Option<OffsetDateTime>,
296 pub min_price: u64,
297 pub sale: Option<GameSale>,
298 #[serde(deserialize_with = "empty_object_as_vec")]
299 pub traits: Vec<GameTrait>,
300}
301
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303pub struct Game {
304 #[serde(flatten)]
305 pub game_info: GameCommon,
306 pub user: User,
307}
308
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310pub struct Collection {
311 pub id: CollectionID,
312 pub title: String,
313 pub games_count: u64,
314 #[serde(with = "rfc3339")]
315 pub created_at: OffsetDateTime,
316 #[serde(with = "rfc3339")]
317 pub updated_at: OffsetDateTime,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321pub struct CollectionGame {
322 #[serde(flatten)]
323 pub game_info: GameCommon,
324}
325
326#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
327pub struct CollectionGameItem {
328 pub game: CollectionGame,
329 pub position: u64,
330 pub user_id: UserID,
331 #[serde(with = "rfc3339")]
332 pub created_at: OffsetDateTime,
333}
334
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct CreatedGame {
337 #[serde(flatten)]
338 pub game_info: GameCommon,
339 pub user: User,
340 pub views_count: u64,
341 pub purchases_count: u64,
342 pub downloads_count: u64,
343 pub published: bool,
344}
345
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct OwnedKey {
348 pub id: OwnedKeyID,
349 pub game_id: GameID,
350 pub downloads: u64,
351 pub game: Game,
352 #[serde(with = "rfc3339")]
353 pub created_at: OffsetDateTime,
354 #[serde(with = "rfc3339")]
355 pub updated_at: OffsetDateTime,
356}
357
358#[serde_as]
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363pub struct BuildCommon {
364 pub id: BuildID,
365 #[serde_as(deserialize_as = "DefaultOnError")]
366 pub parent_build_id: Option<BuildID>,
367 pub version: u64,
368 pub user_version: Option<String>,
369 #[serde(with = "rfc3339")]
370 pub created_at: OffsetDateTime,
371 #[serde(with = "rfc3339")]
372 pub updated_at: OffsetDateTime,
373}
374
375#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
376#[serde(rename_all = "snake_case")]
377pub enum BuildFileType {
378 Archive,
379 Patch,
380 Signature,
381 Manifest,
382 Unpacked,
383}
384
385#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum BuildFileSubtype {
388 Default,
389 Optimized,
390 Accelerated,
391 Gzip,
392}
393
394#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
395#[serde(rename_all = "snake_case")]
396pub enum BuildFileState {
397 Uploaded,
398}
399
400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
401pub struct BuildFile {
402 pub size: u64,
403 pub r#type: BuildFileType,
404 pub sub_type: BuildFileSubtype,
405 pub state: BuildFileState,
406}
407
408#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum BuildState {
411 Completed,
412}
413
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
415pub struct Build {
416 #[serde(flatten)]
417 pub build_info: BuildCommon,
418 pub upload_id: UploadID,
419 pub user: User,
420 pub state: BuildState,
421 #[serde(deserialize_with = "empty_object_as_vec")]
422 pub files: Vec<BuildFile>,
423}
424
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426pub struct UpgradePathBuild {
427 #[serde(flatten)]
428 pub build_info: BuildCommon,
429 pub upload_id: UploadID,
430 #[serde(deserialize_with = "empty_object_as_vec")]
431 pub files: Vec<BuildFile>,
432}
433
434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
435pub struct UploadBuild {
436 #[serde(flatten)]
437 pub build_info: BuildCommon,
438}
439
440#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
441#[serde(rename_all = "snake_case")]
442pub enum UploadType {
443 Default,
444 Html,
445 Flash,
446 Java,
447 Unity,
448 Soundtrack,
449 Book,
450 Video,
451 Documentation,
452 Mod,
453 AudioAssets,
454 GraphicalAssets,
455 Sourcecode,
456 Other,
457}
458
459#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
460#[serde(rename_all = "snake_case")]
461pub enum UploadTrait {
462 PLinux,
463 PWindows,
464 POsx,
465 PAndroid,
466 Demo,
467}
468
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
470#[serde(tag = "storage", rename_all = "snake_case")]
471pub enum UploadStorage {
472 Hosted {
473 size: u64,
474 md5_hash: Option<String>,
475 },
476 Build {
477 size: u64,
478 build: UploadBuild,
479 build_id: BuildID,
480 channel_name: String,
481 },
482 External {
483 host: String,
484 },
485}
486
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
488pub struct Upload {
489 pub position: u64,
490 pub id: UploadID,
491 pub game_id: GameID,
492 pub r#type: UploadType,
493 #[serde(deserialize_with = "empty_object_as_vec")]
494 pub traits: Vec<UploadTrait>,
495 pub filename: String,
496 pub display_name: Option<String>,
497 #[serde(flatten)]
498 pub storage: UploadStorage,
499 #[serde(with = "rfc3339")]
500 pub created_at: OffsetDateTime,
501 #[serde(with = "rfc3339")]
502 pub updated_at: OffsetDateTime,
503}
504
505impl Upload {
506 #[must_use]
508 pub fn get_name(&self) -> &str {
509 self.display_name.as_deref().unwrap_or(&self.filename)
510 }
511
512 #[must_use]
514 pub fn get_hash(&self) -> Option<&str> {
515 match &self.storage {
516 UploadStorage::Hosted { md5_hash, .. } => md5_hash.as_deref(),
517 _ => None,
518 }
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
523#[serde(rename_all = "kebab-case")]
524pub enum ManifestActionPlatform {
525 Linux,
526 Windows,
527 Osx,
528 Unknown,
529}
530
531#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
532pub struct ManifestAction {
533 pub name: String,
534 pub path: String,
535 pub platform: Option<ManifestActionPlatform>,
536 pub args: Option<Vec<String>>,
537 pub sandbox: Option<bool>,
538 pub console: Option<bool>,
539 pub scope: Option<String>,
541}
542
543#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544pub enum ManifestPrerequisiteName {
545 #[serde(rename = "vcredist-2010-x64")]
546 Vcredist2010x64,
547 #[serde(rename = "vcredist-2010-x86")]
548 Vcredist2010x86,
549 #[serde(rename = "vcredist-2013-x64")]
550 Vcredist2013x64,
551 #[serde(rename = "vcredist-2013-x86")]
552 Vcredist2013x86,
553 #[serde(rename = "vcredist-2015-x64")]
554 Vcredist2015x64,
555 #[serde(rename = "vcredist-2015-x86")]
556 Vcredist2015x86,
557 #[serde(rename = "vcredist-2017-x64")]
558 Vcredist2017x64,
559 #[serde(rename = "vcredist-2017-x86")]
560 Vcredist2017x86,
561 #[serde(rename = "vcredist-2019-x64")]
562 Vcredist2019x64,
563 #[serde(rename = "vcredist-2019-x86")]
564 Vcredist2019x86,
565
566 #[serde(rename = "net-4.5.2")]
567 Net452,
568 #[serde(rename = "net-4.6")]
569 Net46,
570 #[serde(rename = "net-4.6.2")]
571 Net462,
572
573 #[serde(rename = "xna-4.0")]
574 Xna40,
575
576 #[serde(rename = "dx-june-2010")]
577 DxJune2010,
578}
579
580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
581pub struct ManifestPrerequisite {
582 pub name: ManifestPrerequisiteName,
583}
584
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
586pub struct Manifest {
587 pub actions: Option<Vec<ManifestAction>>,
588 pub prereqs: Option<Vec<ManifestPrerequisite>>,
589}
590
591#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
592#[serde(tag = "object_type", rename_all = "snake_case")]
593pub enum ScannedArchiveObject {
594 Upload { object_id: UploadID },
595 Build { object_id: BuildID },
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
599pub struct ScannedArchive {
600 #[serde(flatten)]
601 pub object_type: ScannedArchiveObject,
602 pub extracted_size: Option<u64>,
603 pub manifest: Option<Manifest>,
604 #[serde(with = "rfc3339")]
607 pub created_at: OffsetDateTime,
608 #[serde(with = "rfc3339")]
609 pub updated_at: OffsetDateTime,
610}