1pub mod api;
5
6use api::{ApiToken, FullModSpec};
7use bytes::Bytes;
8use elsa::FrozenMap;
9use semver::Version;
10use thiserror::Error;
11use tracing::info;
12
13use crate::api::LoginResponse;
14
15pub struct ModPortalClient {
18 client: reqwest::Client,
19 specs: FrozenMap<String, Box<FullModSpec>>,
20}
21
22impl ModPortalClient {
23 pub fn new() -> Result<ModPortalClient> {
25 ModPortalClient::with_client(reqwest::Client::builder().build()?)
26 }
27
28 pub fn with_client(client: reqwest::Client) -> Result<ModPortalClient> {
30 Ok(ModPortalClient { client, specs: FrozenMap::new() })
31 }
32
33 pub async fn get_mod_spec(&self, name: &str) -> Result<&FullModSpec> {
48 Ok(if let Some(spec) = self.specs.get(name) {
49 info!("returning mod spec for '{name}' from cache");
50 spec
51 } else {
52 info!("requesting mod spec for '{name}'");
53 let url = format!("https://mods.factorio.com/api/mods/{name}/full");
54 let response = self.client.get(url).send().await?.json().await?;
55 self.specs.insert(name.into(), Box::new(response))
56 })
57 }
58
59 pub async fn login(&self, user_name: &str, password: &str) -> Result<ApiToken> {
76 info!("logging in with user name '{user_name}'");
77
78 let url = "https://auth.factorio.com/api-login";
79 let query = [("api_version", "4"), ("username", user_name), ("password", password)];
80
81 let request = self.client.post(url).query(&query);
82 let response = request.send().await?.json().await?;
83
84 match response {
85 LoginResponse::Success { token } => Ok(token),
86 LoginResponse::Error { error, message } => {
87 Err(FactorioModApiError::LoginError { error, message })
88 }
89 }
90 }
91
92 pub async fn download_mod(
111 &self,
112 mod_name: &str,
113 version: &Version,
114 api_token: &ApiToken,
115 ) -> Result<Bytes> {
116 info!("downloading version {version} of '{mod_name}' mod");
117
118 let releases = &self.get_mod_spec(mod_name).await?.short_spec.releases;
119 let Some(release) = releases.iter().find(|r| r.version == *version) else {
120 return Err(FactorioModApiError::InvalidModVersion { version: version.clone() })
121 };
122
123 let url = format!("https://mods.factorio.com/{}", release.download_url);
124 let query = [("username", &api_token.username), ("token", &api_token.token)];
125
126 Ok(self.client.get(url).query(&query).send().await?.bytes().await?)
127 }
128}
129
130pub type Result<T> = std::result::Result<T, FactorioModApiError>;
132
133#[derive(Error, Debug)]
135pub enum FactorioModApiError {
136 #[error("Invalid mod dependency: '{dep}'")]
138 InvalidModDependency { dep: String },
139
140 #[error("Invalid mod version: '{version}'")]
142 InvalidModVersion { version: Version },
143
144 #[error("Error while talking to the API Server")]
146 RequestError(#[from] reqwest::Error),
147
148 #[error("Error while parsing a version number")]
150 VersionError(#[from] semver::Error),
151
152 #[error("failed to parse JSON")]
154 JsonParsingError(#[from] serde_json::Error),
155
156 #[error("failed to log in: {error}, {message}")]
157 LoginError { error: String, message: String },
158}