use std::time::Duration;
use reqwest::{Client, StatusCode, header::{self, HeaderValue}};
use thiserror::Error;
use serde::{Deserialize, de::DeserializeOwned};
use url::Url;
mod parsers;
pub mod credentials_info;
pub mod download_keys;
pub mod me;
pub mod my_games;
pub mod purchases;
pub mod search;
pub mod users;
pub mod wharf;
#[derive(Clone, Debug, Deserialize)]
pub struct SmallUser {
pub id: u32,
pub username: String,
pub display_name: Option<String>,
pub url: Url,
pub cover_url: Option<Url>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct User {
pub username: String,
pub display_name: Option<String>,
pub url: Url,
pub cover_url: Option<Url>,
pub press_user: bool,
pub developer: bool,
pub gamer: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Embed {
pub width: u32,
pub height: u32,
pub fullscreen: bool,
}
pub struct Itchio {
client: Client,
}
impl Itchio {
pub fn new(key: String) -> Result<Self, ItchioError> {
let mut headers = header::HeaderMap::new();
let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", key))
.map_err(|err| ItchioError::ClientCreationError(err.to_string()))?;
auth_value.set_sensitive(true);
headers.insert("Authorization", auth_value);
let client = Client::builder()
.timeout(Duration::from_secs(30))
.default_headers(headers)
.user_agent("itchio-api (https://codeberg.org/Taevas/itchio-api)")
.build()
.map_err(|err| ItchioError::ClientCreationError(err.to_string()))?;
Ok(Self {
client,
})
}
pub async fn request<T: DeserializeOwned>(&self, endpoint: String) -> Result<T, ItchioError> {
let url = format!("https://itch.io/api/1/key/{}", endpoint);
let response = self
.client
.get(&url)
.send()
.await
.map_err(|err| {
match err.status() {
Some(status) => match status {
StatusCode::NOT_FOUND => ItchioError::BadEndpoint,
StatusCode::TOO_MANY_REQUESTS => ItchioError::RateLimited,
_ => ItchioError::RequestFailed(err),
}
None => ItchioError::RequestFailed(err),
}
})?;
let text = response
.text()
.await?;
if let Ok(bad_response) = serde_json::from_str::<ErrorResponse>(&text) {
if let Some(err_zero) = bad_response.errors.get(0) {
let error = match err_zero.as_str() {
"invalid key" => ItchioError::BadKey,
"missing authorization header" => ItchioError::BadKey,
"invalid api endpoint" => ItchioError::BadEndpoint, "invalid user" => ItchioError::NotFound,
"invalid game" => ItchioError::NotFound,
"invalid game_id" => ItchioError::NotFound, "no download key found" => ItchioError::NotFound,
other => ItchioError::InternalError(other.to_string()),
};
return Err(error);
} else {
let message = bad_response.details.unwrap_or("Somehow received an empty error".to_string());
return Err(ItchioError::InternalError(message))
}
}
Ok(serde_json::from_str::<T>(&text)?)
}
}
#[derive(Deserialize)]
struct ErrorResponse {
errors: Vec<String>,
details: Option<String>,
}
#[derive(Error, Debug)]
pub enum ItchioError {
#[error("Couldn't create a client: {0}")]
ClientCreationError(String),
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("Failed to parse JSON: {0}")]
JsonParseError(#[from] serde_json::Error),
#[error("An unexpected error happened: {0}")]
InternalError(String),
#[error("Authentication failed.")]
BadKey,
#[error("The requested endpoint doesn't exist.")]
BadEndpoint,
#[error("The requested data doesn't exist.")]
NotFound,
#[error("The server is rate limiting us.")]
RateLimited,
}