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#[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#[derive(Clone, Debug, Deserialize)] #[allow(dead_code)]
30pub struct Embed {
31 pub width: u32,
32 pub height: u32,
33 pub fullscreen: bool,
34}
35
36pub struct Itchio {
38 client: Client,
39 key: String,
40}
41
42impl Itchio {
43 pub fn new(key: String) -> Self {
45 Self {
46 client: Client::new(),
47 key,
48 }
49 }
50
51 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 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, "invalid user" => ItchioError::NotFound,
85 "invalid game" => ItchioError::NotFound,
86 "invalid game_id" => ItchioError::NotFound, "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#[derive(Error, Debug)]
109pub enum ItchioError {
110 #[error("HTTP request failed: {0}")]
112 RequestFailed(#[from] reqwest::Error),
113 #[error("Failed to parse JSON: {0}")]
115 JsonParseError(#[from] serde_json::Error),
116 #[error("An unexpected error happened: {0}")]
118 InternalError(String),
119 #[error("Authentication failed.")]
121 BadKey,
122 #[error("The requested endpoint doesn't exist.")]
124 BadEndpoint,
125 #[error("The requested data doesn't exist.")]
127 NotFound,
128 #[error("The server is rate limiting us.")]
130 RateLimited,
131}