pub mod errors;
mod responses;
pub mod types;
use errors::*;
use responses::*;
pub use responses::{ApiResponse, IntoResponseResult, LoginResponse};
use types::*;
use reqwest::{Method, Response, header};
#[derive(Debug, Clone)]
pub struct ItchClient {
client: reqwest::Client,
api_key: String,
}
impl ItchClient {
pub(crate) async fn itch_request(
&self,
url: &ItchApiUrl,
method: Method,
options: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
) -> Result<Response, reqwest::Error> {
let mut request: reqwest::RequestBuilder = self.client.request(method, url.as_str());
request = match url.get_version() {
ItchApiVersion::V1 => {
request.header(header::AUTHORIZATION, format!("Bearer {}", &self.api_key))
}
ItchApiVersion::V2 => request.header(header::AUTHORIZATION, &self.api_key),
ItchApiVersion::Other => request,
};
if let ItchApiVersion::V2 = url.get_version() {
request = request.header(header::ACCEPT, "application/vnd.itch.v2");
}
request = options(request);
request.send().await
}
async fn itch_request_json<T>(
&self,
url: &ItchApiUrl,
method: Method,
options: impl FnOnce(reqwest::RequestBuilder) -> reqwest::RequestBuilder,
) -> Result<T, ItchRequestJSONError<<T as IntoResponseResult>::Err>>
where
T: serde::de::DeserializeOwned + IntoResponseResult,
{
let text = self
.itch_request(url, method, options)
.await
.map_err(|e| ItchRequestJSONError {
url: url.to_string(),
kind: ItchRequestJSONErrorKind::CouldntSend(e),
})?
.text()
.await
.map_err(|e| ItchRequestJSONError {
url: url.to_string(),
kind: ItchRequestJSONErrorKind::CouldntGetText(e),
})?;
serde_json::from_str::<ApiResponse<T>>(&text)
.map_err(|error| ItchRequestJSONError {
url: url.to_string(),
kind: ItchRequestJSONErrorKind::InvalidJSON { body: text, error },
})?
.into_result()
.map_err(|e| ItchRequestJSONError {
url: url.to_string(),
kind: ItchRequestJSONErrorKind::ServerRepliedWithError(e),
})
}
}
impl ItchClient {
#[must_use]
pub fn get_api_key(&self) -> &str {
&self.api_key
}
#[must_use]
pub fn new(api_key: String) -> Self {
Self {
client: reqwest::Client::new(),
api_key,
}
}
pub async fn auth(
api_key: String,
) -> Result<Self, ItchRequestJSONError<ApiResponseCommonErrors>> {
let client = ItchClient::new(api_key);
get_profile(&client).await?;
Ok(client)
}
}
pub async fn login(
client: &ItchClient,
username: &str,
password: &str,
recaptcha_response: Option<&str>,
) -> Result<LoginResponse, ItchRequestJSONError<LoginResponseError>> {
let mut params: Vec<(&'static str, &str)> = vec![
("username", username),
("password", password),
("force_recaptcha", "false"),
("source", "desktop"),
];
if let Some(rr) = recaptcha_response {
params.push(("recaptcha_response", rr));
}
client
.itch_request_json::<LoginResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "login"),
Method::POST,
|b| b.form(¶ms),
)
.await
}
pub async fn totp_verification(
client: &ItchClient,
totp_token: &str,
totp_code: u64,
) -> Result<LoginSuccess, ItchRequestJSONError<TOTPResponseError>> {
client
.itch_request_json::<TOTPResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "totp/verify"),
Method::POST,
|b| b.form(&[("token", totp_token), ("code", &totp_code.to_string())]),
)
.await
.map(|res| res.success)
}
pub async fn get_user_info(
client: &ItchClient,
user_id: UserID,
) -> Result<User, ItchRequestJSONError<UserResponseError>> {
client
.itch_request_json::<UserInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("users/{user_id}")),
Method::GET,
|b| b,
)
.await
.map(|res| res.user)
}
pub async fn get_profile(
client: &ItchClient,
) -> Result<Profile, ItchRequestJSONError<ApiResponseCommonErrors>> {
client
.itch_request_json::<ProfileInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "profile"),
Method::GET,
|b| b,
)
.await
.map(|res| res.user)
}
pub async fn get_created_games(
client: &ItchClient,
) -> Result<Vec<CreatedGame>, ItchRequestJSONError<ApiResponseCommonErrors>> {
client
.itch_request_json::<CreatedGamesResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "profile/games"),
Method::GET,
|b| b,
)
.await
.map(|res| res.games)
}
pub async fn get_owned_keys(
client: &ItchClient,
) -> Result<Vec<OwnedKey>, ItchRequestJSONError<ApiResponseCommonErrors>> {
let mut values: Vec<OwnedKey> = Vec::new();
let mut page: u64 = 1;
loop {
let response = client
.itch_request_json::<OwnedKeysResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "profile/owned-keys"),
Method::GET,
|b| b.query(&[("page", page)]),
)
.await?;
let response_values = response.owned_keys;
let num_elements: u64 = response_values.len() as u64;
values.extend(response_values.into_iter());
if num_elements == 0 || num_elements < response.per_page {
break;
}
page += 1;
}
Ok(values)
}
pub async fn get_profile_collections(
client: &ItchClient,
) -> Result<Vec<Collection>, ItchRequestJSONError<ApiResponseCommonErrors>> {
client
.itch_request_json::<ProfileCollectionsResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, "profile/collections"),
Method::GET,
|b| b,
)
.await
.map(|res| res.collections)
}
pub async fn get_collection_info(
client: &ItchClient,
collection_id: CollectionID,
) -> Result<Collection, ItchRequestJSONError<CollectionResponseError>> {
client
.itch_request_json::<CollectionInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("collections/{collection_id}")),
Method::GET,
|b| b,
)
.await
.map(|res| res.collection)
}
pub async fn get_collection_games(
client: &ItchClient,
collection_id: CollectionID,
) -> Result<Vec<CollectionGameItem>, ItchRequestJSONError<CollectionResponseError>> {
let mut values: Vec<CollectionGameItem> = Vec::new();
let mut page: u64 = 1;
loop {
let response = client
.itch_request_json::<CollectionGamesResponse>(
&ItchApiUrl::from_api_endpoint(
ItchApiVersion::V2,
format!("collections/{collection_id}/collection-games"),
),
Method::GET,
|b| b.query(&[("page", page)]),
)
.await?;
let response_values = response.collection_games;
let num_elements: u64 = response_values.len() as u64;
values.extend(response_values.into_iter());
if num_elements == 0 || num_elements < response.per_page {
break;
}
page += 1;
}
Ok(values)
}
pub async fn get_game_info(
client: &ItchClient,
game_id: GameID,
) -> Result<Game, ItchRequestJSONError<GameResponseError>> {
client
.itch_request_json::<GameInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("games/{game_id}")),
Method::GET,
|b| b,
)
.await
.map(|res| res.game)
}
pub async fn get_game_uploads(
client: &ItchClient,
game_id: GameID,
) -> Result<Vec<Upload>, ItchRequestJSONError<GameResponseError>> {
client
.itch_request_json::<GameUploadsResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("games/{game_id}/uploads")),
Method::GET,
|b| b,
)
.await
.map(|res| res.uploads)
}
pub async fn get_upload_info(
client: &ItchClient,
upload_id: UploadID,
) -> Result<Upload, ItchRequestJSONError<UploadResponseError>> {
client
.itch_request_json::<UploadInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("uploads/{upload_id}")),
Method::GET,
|b| b,
)
.await
.map(|res| res.upload)
}
pub async fn get_upload_builds(
client: &ItchClient,
upload_id: UploadID,
) -> Result<Vec<UploadBuild>, ItchRequestJSONError<UploadResponseError>> {
client
.itch_request_json::<UploadBuildsResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("uploads/{upload_id}/builds")),
Method::GET,
|b| b,
)
.await
.map(|res| res.builds)
}
pub async fn get_build_info(
client: &ItchClient,
build_id: BuildID,
) -> Result<Build, ItchRequestJSONError<BuildResponseError>> {
client
.itch_request_json::<BuildInfoResponse>(
&ItchApiUrl::from_api_endpoint(ItchApiVersion::V2, format!("builds/{build_id}")),
Method::GET,
|b| b,
)
.await
.map(|res| res.build)
}
pub async fn get_upgrade_path(
client: &ItchClient,
current_build_id: BuildID,
target_build_id: BuildID,
) -> Result<Vec<UpgradePathBuild>, ItchRequestJSONError<UpgradePathResponseError>> {
client
.itch_request_json::<BuildUpgradePathResponse>(
&ItchApiUrl::from_api_endpoint(
ItchApiVersion::V2,
format!("builds/{current_build_id}/upgrade-paths/{target_build_id}"),
),
Method::GET,
|b| b,
)
.await
.map(|res| res.upgrade_path.builds)
}
pub async fn get_upload_scanned_archive(
client: &ItchClient,
upload_id: UploadID,
) -> Result<ScannedArchive, ItchRequestJSONError<UploadResponseError>> {
client
.itch_request_json::<UploadScannedArchiveResponse>(
&ItchApiUrl::from_api_endpoint(
ItchApiVersion::V2,
format!("uploads/{upload_id}/scanned-archive"),
),
Method::GET,
|b| b,
)
.await
.map(|res| res.scanned_archive)
}
pub async fn get_build_scanned_archive(
client: &ItchClient,
build_id: BuildID,
) -> Result<ScannedArchive, ItchRequestJSONError<BuildResponseError>> {
client
.itch_request_json::<BuildScannedArchiveResponse>(
&ItchApiUrl::from_api_endpoint(
ItchApiVersion::V2,
format!("builds/{build_id}/scanned-archive"),
),
Method::GET,
|b| b,
)
.await
.map(|res| res.scanned_archive)
}