diff options
Diffstat (limited to 'src-tauri/src/core/modpack/curseforge.rs')
| -rw-r--r-- | src-tauri/src/core/modpack/curseforge.rs | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/src-tauri/src/core/modpack/curseforge.rs b/src-tauri/src/core/modpack/curseforge.rs new file mode 100644 index 0000000..44a03da --- /dev/null +++ b/src-tauri/src/core/modpack/curseforge.rs @@ -0,0 +1,436 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer, de, de::DeserializeOwned}; + +const CURSEFORGE_API_BASE_URL: &str = "https://api.curseforge.com"; +const CURSEFORGE_API_KEY: Option<&str> = option_env!("CURSEFORGE_API_KEY"); + +macro_rules! curseforge_int_enum { + ( + $vis:vis enum $name:ident : $repr:ty { + $($variant:ident = $value:expr),+ $(,)? + } + ) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[repr($repr)] + $vis enum $name { + $($variant = $value),+ + } + + impl TryFrom<$repr> for $name { + type Error = $repr; + + fn try_from(value: $repr) -> Result<Self, Self::Error> { + match value { + $($value => Ok(Self::$variant),)+ + _ => Err(value), + } + } + } + + impl Serialize for $name { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (*self as $repr).serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for $name { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let value = <$repr>::deserialize(deserializer)?; + Self::try_from(value).map_err(|value| { + de::Error::custom(format!("invalid {} value: {value}", stringify!($name))) + }) + } + } + }; +} + +#[derive(Debug, Clone)] +pub(crate) struct CurseForgeApi { + client: reqwest::Client, +} + +impl CurseForgeApi { + pub(crate) fn new(client: reqwest::Client) -> Self { + Self { client } + } + + pub(crate) async fn get_files( + &self, + request: &CurseForgeGetModFilesRequestBody, + ) -> Result<CurseForgeGetFilesResponse, String> { + if request.file_ids.is_empty() { + return Ok(CurseForgeGetFilesResponse::default()); + } + + self.post("/v1/mods/files", request).await + } + + pub(crate) async fn get_mods( + &self, + request: &CurseForgeGetModsByIdsListRequestBody, + ) -> Result<CurseForgeGetModsResponse, String> { + if request.mod_ids.is_empty() { + return Ok(CurseForgeGetModsResponse::default()); + } + + self.post("/v1/mods", request).await + } + + async fn post<TRequest, TResponse>( + &self, + endpoint: &str, + body: &TRequest, + ) -> Result<TResponse, String> + where + TRequest: Serialize + ?Sized, + TResponse: DeserializeOwned, + { + let api_key = CURSEFORGE_API_KEY + .ok_or("CurseForge modpack support requires CURSEFORGE_API_KEY set at build time")?; + let response = self + .client + .post(format!("{CURSEFORGE_API_BASE_URL}{endpoint}")) + .header("x-api-key", api_key) + .json(body) + .send() + .await + .map_err(|e| format!("CurseForge API error: {e}"))?; + + if !response.status().is_success() { + return Err(format!("CurseForge API returned {}", response.status())); + } + + response.json().await.map_err(|e| e.to_string()) + } +} + +impl Default for CurseForgeApi { + fn default() -> Self { + Self::new(reqwest::Client::new()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeGetModFilesRequestBody { + pub(crate) file_ids: Vec<u64>, +} + +impl CurseForgeGetModFilesRequestBody { + pub(crate) fn new(file_ids: Vec<u64>) -> Self { + Self { file_ids } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeGetModsByIdsListRequestBody { + pub(crate) mod_ids: Vec<u64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) filter_pc_only: Option<bool>, +} + +impl CurseForgeGetModsByIdsListRequestBody { + pub(crate) fn new(mod_ids: Vec<u64>) -> Self { + Self { + mod_ids, + filter_pc_only: None, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub(crate) struct CurseForgeGetFilesResponse { + #[serde(default)] + pub(crate) data: Vec<CurseForgeFile>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub(crate) struct CurseForgeGetModsResponse { + #[serde(default)] + pub(crate) data: Vec<CurseForgeMod>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeCategory { + pub(crate) id: u64, + pub(crate) game_id: u64, + pub(crate) name: String, + pub(crate) slug: String, + pub(crate) url: String, + pub(crate) icon_url: String, + pub(crate) date_modified: String, + pub(crate) is_class: Option<bool>, + pub(crate) class_id: Option<u64>, + pub(crate) parent_category_id: Option<u64>, + pub(crate) display_index: Option<u64>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeModLinks { + pub(crate) website_url: String, + pub(crate) wiki_url: String, + pub(crate) issues_url: String, + pub(crate) source_url: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeModAuthor { + pub(crate) id: u64, + pub(crate) name: String, + pub(crate) url: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeModAsset { + pub(crate) id: u64, + pub(crate) mod_id: u64, + pub(crate) title: String, + pub(crate) description: String, + pub(crate) thumbnail_url: String, + pub(crate) url: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeFileHash { + pub(crate) value: String, + pub(crate) algo: CurseForgeHashAlgo, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeSortableGameVersion { + pub(crate) game_version_name: String, + pub(crate) game_version_padded: String, + pub(crate) game_version: String, + pub(crate) game_version_release_date: String, + pub(crate) game_version_type_id: Option<u64>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeFileDependency { + pub(crate) mod_id: u64, + pub(crate) relation_type: CurseForgeFileRelationType, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeFileModule { + pub(crate) name: String, + pub(crate) fingerprint: u64, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeFileIndex { + pub(crate) game_version: String, + pub(crate) file_id: u64, + pub(crate) filename: String, + pub(crate) release_type: CurseForgeFileReleaseType, + pub(crate) game_version_type_id: Option<u64>, + pub(crate) mod_loader: CurseForgeModLoaderType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeFile { + pub(crate) id: u64, + pub(crate) game_id: u64, + pub(crate) mod_id: u64, + pub(crate) is_available: bool, + pub(crate) display_name: String, + pub(crate) file_name: String, + pub(crate) release_type: CurseForgeFileReleaseType, + pub(crate) file_status: CurseForgeFileStatus, + #[serde(default)] + pub(crate) hashes: Vec<CurseForgeFileHash>, + pub(crate) file_date: String, + pub(crate) file_length: u64, + pub(crate) download_count: u64, + pub(crate) file_size_on_disk: Option<u64>, + #[serde(default)] + pub(crate) download_url: Option<String>, + #[serde(default)] + pub(crate) game_versions: Vec<String>, + #[serde(default)] + pub(crate) sortable_game_versions: Vec<CurseForgeSortableGameVersion>, + #[serde(default)] + pub(crate) dependencies: Vec<CurseForgeFileDependency>, + pub(crate) expose_as_alternative: Option<bool>, + pub(crate) parent_project_file_id: Option<u64>, + pub(crate) alternate_file_id: Option<u64>, + pub(crate) is_server_pack: Option<bool>, + pub(crate) server_pack_file_id: Option<u64>, + pub(crate) is_early_access_content: Option<bool>, + pub(crate) early_access_end_date: Option<String>, + pub(crate) file_fingerprint: u64, + #[serde(default)] + pub(crate) modules: Vec<CurseForgeFileModule>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CurseForgeMod { + pub(crate) id: u64, + pub(crate) game_id: u64, + pub(crate) name: String, + pub(crate) slug: String, + #[serde(default)] + pub(crate) links: CurseForgeModLinks, + pub(crate) summary: String, + pub(crate) status: CurseForgeModStatus, + pub(crate) download_count: u64, + pub(crate) is_featured: bool, + pub(crate) primary_category_id: u64, + #[serde(default)] + pub(crate) categories: Vec<CurseForgeCategory>, + pub(crate) class_id: Option<u64>, + #[serde(default)] + pub(crate) authors: Vec<CurseForgeModAuthor>, + pub(crate) logo: Option<CurseForgeModAsset>, + #[serde(default)] + pub(crate) screenshots: Vec<CurseForgeModAsset>, + pub(crate) main_file_id: u64, + #[serde(default)] + pub(crate) latest_files: Vec<CurseForgeFile>, + #[serde(default)] + pub(crate) latest_files_indexes: Vec<CurseForgeFileIndex>, + #[serde(default)] + pub(crate) latest_early_access_files_indexes: Vec<CurseForgeFileIndex>, + pub(crate) date_created: String, + pub(crate) date_modified: String, + pub(crate) date_released: String, + pub(crate) allow_mod_distribution: Option<bool>, + pub(crate) game_popularity_rank: u64, + pub(crate) is_available: bool, + pub(crate) thumbs_up_count: u64, + pub(crate) rating: Option<f64>, +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeHashAlgo: u8 { + Sha1 = 1, + Md5 = 2 + } +} + +impl Default for CurseForgeHashAlgo { + fn default() -> Self { + Self::Sha1 + } +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeFileRelationType: u8 { + EmbeddedLibrary = 1, + OptionalDependency = 2, + RequiredDependency = 3, + Tool = 4, + Incompatible = 5, + Include = 6 + } +} + +impl Default for CurseForgeFileRelationType { + fn default() -> Self { + Self::RequiredDependency + } +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeFileReleaseType: u8 { + Release = 1, + Beta = 2, + Alpha = 3 + } +} + +impl Default for CurseForgeFileReleaseType { + fn default() -> Self { + Self::Release + } +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeFileStatus: u8 { + Processing = 1, + ChangesRequired = 2, + UnderReview = 3, + Approved = 4, + Rejected = 5, + MalwareDetected = 6, + Deleted = 7, + Archived = 8, + Testing = 9, + Released = 10, + ReadyForReview = 11, + Deprecated = 12, + Baking = 13, + AwaitingPublishing = 14, + FailedPublishing = 15, + Cooking = 16, + Cooked = 17, + UnderManualReview = 18, + ScanningForMalware = 19, + ProcessingFile = 20, + PendingRelease = 21, + ReadyForCooking = 22, + PostProcessing = 23 + } +} + +impl Default for CurseForgeFileStatus { + fn default() -> Self { + Self::Released + } +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeModLoaderType: u8 { + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4, + Quilt = 5, + NeoForge = 6 + } +} + +impl Default for CurseForgeModLoaderType { + fn default() -> Self { + Self::Any + } +} + +curseforge_int_enum! { + pub(crate) enum CurseForgeModStatus: u8 { + New = 1, + ChangesRequired = 2, + UnderSoftReview = 3, + Approved = 4, + Rejected = 5, + ChangesMade = 6, + Inactive = 7, + Abandoned = 8, + Deleted = 9, + UnderReview = 10 + } +} + +impl Default for CurseForgeModStatus { + fn default() -> Self { + Self::Approved + } +} |