Skip to main content

itchio_api/
lib.rs

1// use chrono::{DateTime, Utc};
2use thiserror::Error;
3use serde::{Deserialize};
4use url::Url;
5
6/// What you need to make requests, it stores and uses your API key.
7pub struct Itchio {
8  client: reqwest::Client,
9  key: String,
10}
11
12/// A raw response this crate gets from the server.
13#[derive(Clone, Debug, Deserialize)]
14struct Games {
15  games: Vec<Game>
16}
17
18/// A representation of a Game on the itch.io website.
19#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
20pub struct Game {
21  id: u32,
22  title: String,
23  short_text: Option<String>,
24  url: Url,
25  cover_url: Option<Url>,
26  r#type: String,
27  classification: String,
28  p_linux: bool,
29  p_android: bool,
30  p_windows: bool,
31  p_osx: bool,
32  created_at: String, // TODO DateTime<Utc> from chrono crate
33  min_price: u32,
34  can_be_bought: bool,
35  published: bool,
36  published_at: Option<String>, // TODO DateTime<Utc> from chrono crate
37  has_demo: bool,
38  embed: Option<Embed>,
39  user: User,
40  views_count: u32,
41  purchases_count: u32,
42  downloads_count: u32,
43  in_press_system: bool,
44}
45
46#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
47pub struct User {
48  id: u32,
49  display_name: String,
50  username: String,
51  url: String,
52  cover_url: String,
53}
54
55#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
56pub struct Embed {
57  width: u32,
58  height: u32,
59  fullscreen: bool,
60}
61
62impl Itchio {
63  /// Create a new Itchio client using your API key, which you can find there: <https://itch.io/user/settings/api-keys>
64  pub fn new(key: String) -> Self {
65    Self {
66      client: reqwest::Client::new(),
67      key,
68    }
69  }
70
71  /// Get the games you've uploaded or have edit access to: <https://itch.io/docs/api/serverside#reference/profilegames-httpsitchioapi1keymy-games>
72  pub async fn get_my_games(&self) -> Result<Vec<Game>, ItchioError> {
73    let url = format!("https://itch.io/api/1/{}/my-games", self.key);
74    let response = self
75      .client
76      .get(&url)
77      .send()
78      .await?
79      .json::<Games>()
80      .await?;
81
82    Ok(response.games)
83  }
84}
85
86// TODO Remove RequestFailed, add several other Errors, created from map_err on a request.send
87#[derive(Error, Debug)]
88pub enum ItchioError {
89  /// A generic error about the request itself failing.
90  #[error("HTTP request failed: {0}")]
91  RequestFailed(#[from] reqwest::Error),
92  /// Happens if the API gives us an unexpected object, likely means there's a mistake with this crate.
93  #[error("Failed to parse JSON: {0}")]
94  JsonParseError(#[from] serde_json::Error),
95}
96
97#[cfg(test)]
98mod tests {
99  use super::*;
100  use std::env;
101  use dotenv::dotenv;
102
103  #[tokio::test]
104  async fn get_my_games_ok() {
105    dotenv().ok();
106    let client_secret = env::var("KEY").expect("KEY has to be set");
107    let api = Itchio::new(client_secret);
108    let games = api.get_my_games().await;
109    assert!(games.is_ok())
110  }
111
112  #[tokio::test]
113  async fn get_my_games_bad_key() {
114    let api = Itchio::new("bad_key".to_string());
115    let games = api.get_my_games().await;
116    assert!(games.is_err_and(|err| matches!(err, ItchioError::RequestFailed(_))))
117  }
118}