Skip to main content

itchio_api/
lib.rs

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