Skip to main content

itchio_api/
lib.rs

1use reqwest::{Client, StatusCode};
2use thiserror::Error;
3use serde::{Deserialize, de::DeserializeOwned};
4use url::Url;
5
6mod parsers;
7pub mod credentials_info;
8pub mod download_keys;
9pub mod me;
10pub mod my_games;
11pub mod purchases;
12pub mod search;
13pub mod users;
14pub mod wharf;
15
16/// Details about a User on itch.io.
17#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
18pub struct User {
19  username: String,
20  display_name: Option<String>,
21  url: Url,
22  cover_url: Option<Url>,
23  press_user: bool,
24  developer: bool,
25  gamer: bool,
26}
27
28/// Details about how a Game can be played right from the store page.
29#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
30pub struct Embed {
31  pub width: u32,
32  pub height: u32,
33  pub fullscreen: bool,
34}
35
36/// What you need to make requests, it stores and uses your API key.
37pub struct Itchio {
38  client: Client,
39  key: String,
40}
41
42impl Itchio {
43  /// Create a new Itchio client using your API key, which you can find there: <https://itch.io/user/settings/api-keys>
44  pub fn new(key: String) -> Self {
45    Self {
46      client: Client::new(),
47      key,
48    }
49  }
50
51  /// Make your own requests by specifying an endpoint and struct to deserialize!
52  pub async fn request<T: DeserializeOwned>(&self, endpoint: String) -> Result<T, ItchioError> {
53    let url = format!("https://itch.io/api/1/key/{}", endpoint);
54    let response = self
55      .client
56      .get(&url)
57      .header("Authorization", format!("Bearer {}", self.key))
58      .header("User-Agent", "itchio-api (https://codeberg.org/Taevas/itchio-api)")
59      .send()
60      .await
61      .map_err(|err| {
62        match err.status() {
63          Some(status) => match status {
64            StatusCode::NOT_FOUND => ItchioError::BadEndpoint,
65            StatusCode::TOO_MANY_REQUESTS => ItchioError::RateLimited,
66            _ => ItchioError::RequestFailed(err),
67          }
68          None => ItchioError::RequestFailed(err),
69        }
70      })?;
71
72    // We cannot consume the response twice, so instead of trying to call .json() twice,
73    // let's use serde_json twice to consume references to a String containing what we want from the Response!
74    let text = response
75      .text()
76      .await?;
77
78    if let Ok(bad_response) = serde_json::from_str::<ErrorResponse>(&text) {
79      if let Some(err_zero) = bad_response.errors.get(0) {
80        let error = match err_zero.as_str() {
81          "invalid key" => ItchioError::BadKey,
82          "missing authorization header" => ItchioError::BadKey,
83          "invalid api endpoint" => ItchioError::BadEndpoint, // should have already been handled by 404 status
84          "invalid user" => ItchioError::NotFound,
85          "invalid game" => ItchioError::NotFound,
86          "invalid game_id" => ItchioError::NotFound, // applies also to game_ids that exist but unauthorized
87          "no download key found" => ItchioError::NotFound,
88          other => ItchioError::InternalError(other.to_string()),
89        };
90        return Err(error);
91      } else {
92        let message = bad_response.details.unwrap_or("Somehow received an empty error".to_string());
93        return Err(ItchioError::InternalError(message))
94      }
95    }
96
97    Ok(serde_json::from_str::<T>(&text)?)
98  }
99}
100
101#[derive(Deserialize)]
102struct ErrorResponse {
103  errors: Vec<String>,
104  details: Option<String>,
105}
106
107/// When something goes wrong, an ItchioError is used to describe what happened.
108#[derive(Error, Debug)]
109pub enum ItchioError {
110  /// A generic error used when none of the other possible errors are fitting.
111  #[error("HTTP request failed: {0}")]
112  RequestFailed(#[from] reqwest::Error),
113  /// Happens if the API gives us an unexpected object, likely means there's a mistake with this crate (or a bad struct given to request()).
114  #[error("Failed to parse JSON: {0}")]
115  JsonParseError(#[from] serde_json::Error),
116  /// Due to certain circumstances, no other error can be used.
117  #[error("An unexpected error happened: {0}")]
118  InternalError(String),
119  /// The key that was used was rejected by the server.
120  #[error("Authentication failed.")]
121  BadKey,
122  /// The crate received a 404 status, meaning the endpoint doesn't exist or that there is a mistake with the URL.
123  #[error("The requested endpoint doesn't exist.")]
124  BadEndpoint,
125  /// The crate received a 200 status with a specific error indicating the request is valid but the resource doesn't exist.
126  #[error("The requested data doesn't exist.")]
127  NotFound,
128  /// The crate received a 429 status, meaning we sent too many requests in a given amount of time.
129  #[error("The server is rate limiting us.")]
130  RateLimited,
131}