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#[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#[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#[derive(Clone, Debug, Deserialize)]
42pub struct Embed {
43 pub width: u32,
44 pub height: u32,
45 pub fullscreen: bool,
46}
47
48pub struct Itchio {
50 client: Client,
51}
52
53impl Itchio {
54 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 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 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, "invalid user" => ItchioError::NotFound,
109 "invalid game" => ItchioError::NotFound,
110 "invalid game_id" => ItchioError::NotFound, "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#[derive(Error, Debug)]
133pub enum ItchioError {
134 #[error("Couldn't create a client: {0}")]
136 ClientCreationError(String),
137 #[error("HTTP request failed: {0}")]
139 RequestFailed(#[from] reqwest::Error),
140 #[error("Failed to parse JSON: {0}")]
142 JsonParseError(#[from] serde_json::Error),
143 #[error("An unexpected error happened: {0}")]
145 InternalError(String),
146 #[error("Authentication failed.")]
148 BadKey,
149 #[error("The requested endpoint doesn't exist.")]
151 BadEndpoint,
152 #[error("The requested data doesn't exist.")]
154 NotFound,
155 #[error("The server is rate limiting us.")]
157 RateLimited,
158}