pub mod api;
use api::{ApiToken, FullModSpec};
use bytes::Bytes;
use elsa::FrozenMap;
use semver::Version;
use thiserror::Error;
use tracing::info;
use crate::api::LoginResponse;
pub struct ModPortalClient {
client: reqwest::Client,
specs: FrozenMap<String, Box<FullModSpec>>,
}
impl ModPortalClient {
pub fn new() -> Result<ModPortalClient> {
ModPortalClient::with_client(reqwest::Client::builder().build()?)
}
pub fn with_client(client: reqwest::Client) -> Result<ModPortalClient> {
Ok(ModPortalClient { client, specs: FrozenMap::new() })
}
pub async fn get_mod_spec(&self, name: &str) -> Result<&FullModSpec> {
Ok(if let Some(spec) = self.specs.get(name) {
info!("returning mod spec for '{name}' from cache");
spec
} else {
info!("requesting mod spec for '{name}'");
let url = format!("https://mods.factorio.com/api/mods/{name}/full");
let response = self.client.get(url).send().await?.json().await?;
self.specs.insert(name.into(), Box::new(response))
})
}
pub async fn login(&self, user_name: &str, password: &str) -> Result<ApiToken> {
info!("logging in with user name '{user_name}'");
let url = "https://auth.factorio.com/api-login";
let query = [("api_version", "4"), ("username", user_name), ("password", password)];
let request = self.client.post(url).query(&query);
let response = request.send().await?.json().await?;
match response {
LoginResponse::Success { token } => Ok(token),
LoginResponse::Error { error, message } => {
Err(FactorioModApiError::LoginError { error, message })
}
}
}
pub async fn download_mod(
&self,
mod_name: &str,
version: &Version,
api_token: &ApiToken,
) -> Result<Bytes> {
info!("downloading version {version} of '{mod_name}' mod");
let releases = &self.get_mod_spec(mod_name).await?.short_spec.releases;
let Some(release) = releases.iter().find(|r| r.version == *version) else {
return Err(FactorioModApiError::InvalidModVersion { version: version.clone() })
};
let url = format!("https://mods.factorio.com/{}", release.download_url);
let query = [("username", &api_token.username), ("token", &api_token.token)];
Ok(self.client.get(url).query(&query).send().await?.bytes().await?)
}
}
pub type Result<T> = std::result::Result<T, FactorioModApiError>;
#[derive(Error, Debug)]
pub enum FactorioModApiError {
#[error("Invalid mod dependency: '{dep}'")]
InvalidModDependency { dep: String },
#[error("Invalid mod version: '{version}'")]
InvalidModVersion { version: Version },
#[error("Error while talking to the API Server")]
RequestError(#[from] reqwest::Error),
#[error("Error while parsing a version number")]
VersionError(#[from] semver::Error),
#[error("failed to parse JSON")]
JsonParsingError(#[from] serde_json::Error),
#[error("failed to log in: {error}, {message}")]
LoginError { error: String, message: String },
}