diff --git a/.vscode/settings.json b/.vscode/settings.json index d76bd693..6b599fcf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,10 @@ "rust-analyzer.diagnostics.disabled": [ "macro-error" // TODO: Remove this once taurpc upgrades to latest specta ], + "rust-analyzer.assist.preferSelf": true, + "rust-analyzer.procMacro.ignored": { + "async-trait": ["async_trait"] + }, "rust-analyzer.check.command": "check", "rust-analyzer.diagnostics.experimental.enable": false, "rust-analyzer.linkedProjects": [], diff --git a/Cargo.lock b/Cargo.lock index cb7859f6..1a4d9b3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3438,6 +3438,7 @@ dependencies = [ "sea-orm", "serde", "serde_json_path_to_error", + "serde_repr", "serde_with", "sha1", "sha2", @@ -5034,9 +5035,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 90fed91f..66590318 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ serde_with = { version = "=3.12.0", features = [ "json", "macros" ] } +serde_repr = { version = "=0.1.20" } toml = { version = "0.8" } url = { version = "2.5" } uuid = { version = "1.16", features = [ diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index fba93f28..ba162198 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -14,19 +14,17 @@ authors = { workspace = true } path = "src/lib.rs" [features] -default = [ "tauri" ] +default = [ ] tauri = [ "dep:tauri", "dep:taurpc", "specta", "gui", - "onelauncher_macro/tauri", ] specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta", - "onelauncher_macro/specta", ] cli = [ "dep:indicatif" ] gui = [] @@ -51,6 +49,7 @@ tracing-appender = { workspace = true } serde = { workspace = true } serde_json = { package = "serde_json_path_to_error", path = "../serde_json_path_to_error" } serde_with = { workspace = true } +serde_repr = { workspace = true } # utils sea-orm = { workspace = true } diff --git a/packages/core/src/api/packages/categories.rs b/packages/core/src/api/packages/categories.rs new file mode 100644 index 00000000..dda390bd --- /dev/null +++ b/packages/core/src/api/packages/categories.rs @@ -0,0 +1,227 @@ +use onelauncher_entity::package::PackageType; +use serde::{Deserialize, Serialize}; + +use crate::api::packages::provider::ProviderExt; + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PackageCategories { + Mod(Vec), + ResourcePack(Vec), + Shader(Vec), + DataPack(Vec), + ModPack(Vec), +} + +pub trait ToProviderCategory +where + Provider: ProviderExt, +{ + fn as_mod_out(category: &PackageModCategory) -> Out; + fn to_mod(value: &Out) -> Option; + + fn as_resource_pack_out(category: &PackageResourcePackCategory) -> Out; + fn to_resource_pack(value: &Out) -> Option; + + fn as_shader_out(category: &PackageShaderCategory) -> Out; + fn to_shader(value: &Out) -> Option; + + fn as_mod_pack_out(category: &PackageModPackCategory) -> Out; + fn to_mod_pack(value: &Out) -> Option; + + fn as_data_pack_out(category: &PackageModCategory) -> Out { + Self::as_mod_out(category) + } + + fn to_data_pack(value: &Out) -> Option { + Self::to_mod(value) + } + + fn as_out(categories: &PackageCategories) -> Vec { + match categories { + PackageCategories::Mod(categories) => { + categories.into_iter().map(Self::as_mod_out).collect() + } + PackageCategories::ResourcePack(categories) => categories + .into_iter() + .map(Self::as_resource_pack_out) + .collect(), + PackageCategories::Shader(categories) => { + categories.into_iter().map(Self::as_shader_out).collect() + } + PackageCategories::DataPack(categories) => { + categories.into_iter().map(Self::as_data_pack_out).collect() + } + PackageCategories::ModPack(categories) => { + categories.into_iter().map(Self::as_mod_pack_out).collect() + } + } + } + + fn to_list(package_type: &PackageType, values: &Vec) -> PackageCategories { + match package_type { + &PackageType::Mod => { + PackageCategories::Mod(values.iter().filter_map(|v| Self::to_mod(v)).collect()) + } + &PackageType::ResourcePack => PackageCategories::ResourcePack( + values + .iter() + .filter_map(|v| Self::to_resource_pack(v)) + .collect(), + ), + &PackageType::Shader => PackageCategories::Shader( + values.iter().filter_map(|v| Self::to_shader(v)).collect(), + ), + &PackageType::DataPack => PackageCategories::DataPack( + values + .iter() + .filter_map(|v| Self::to_data_pack(v)) + .collect(), + ), + &PackageType::ModPack => PackageCategories::ModPack( + values.iter().filter_map(|v| Self::to_mod_pack(v)).collect(), + ), + } + } +} + +macro_rules! define_category_mapping { + ( + impl $trait_name:ident <$out:ty, $provider:ty> for $struct_name:ty { + $( + $enum_type:ty as $fn_name:ident => $reverse_fn:ident { + $($variant:ident => $value:expr),* $(,)? + } + )* + } + ) => { + impl $trait_name<$out, $provider> for $struct_name { + $( + fn $fn_name(category: &$enum_type) -> $out { + match category { + $( + <$enum_type>::$variant => $value, + )* + } + } + )* + + $( + fn $reverse_fn(value: &$out) -> Option<$enum_type> { + match value { + $( + v if v == &$value => Some(<$enum_type>::$variant), + )* + _ => None, + } + } + )* + } + }; +} + +pub(super) use define_category_mapping; + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PackageModCategory { + Adventure, + Library, + Equipment, + Patches, + Cosmetic, + Food, + Magic, + Information, + Misc, + Performance, + Redstone, + ServerUtil, + Storage, + Technology, + Farming, + Automation, + Transport, + Utility, + QoL, + WorldGen, + Mobs, + Economy, + Social, +} + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PackageResourcePackCategory { + X8, + X16, + X32, + X48, + X64, + X128, + X256, + X512, + + VanillaLike, + Utility, + Tweaks, + Themed, + Simplistic, + Realistic, + Modded, + Decoration, + Cursed, + Combat, + + Audio, + Blocks, + CoreShaders, + Gui, + Fonts, + Equipment, + Environment, + Entities, + Items, + Locale, + Models, +} + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PackageShaderCategory { + VanillaLike, + SemiRealistic, + Realistic, + Fantasy, + Cursed, + Cartoon, + + Bloom, + Atmosphere, + Reflections, + Shadows, + PBR, + PathTracing, + Foliage, + ColoredLightning, + + Potato, + Low, + Medium, + High, + Ultra, +} + +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PackageModPackCategory { + Technology, + Quests, + Optimization, + Multiplayer, + Magic, + LightWeight, + Combat, + Challenging, + Adventure, +} diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index b2f80fa6..f2ed7436 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -4,6 +4,8 @@ use onelauncher_entity::package::{PackageType, Provider}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use crate::api::packages::categories::PackageCategories; + // Divisible by 4 and 3 pub const DEFAULT_LIMIT: usize = 24; @@ -13,8 +15,8 @@ pub const DEFAULT_LIMIT: usize = 24; pub struct Filters { pub game_versions: Option>, pub loaders: Option>, - pub categories: Option>, - pub package_types: Option>, + pub categories: Option, + pub package_type: Option, } #[onelauncher_macro::specta] @@ -53,28 +55,26 @@ pub struct SearchQuery { } #[onelauncher_macro::specta] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SearchResult { pub project_id: String, - pub project_type: String, + pub package_type: PackageType, pub slug: String, pub author: String, pub title: String, pub description: String, - pub categories: Vec, - pub display_categories: Vec, - pub versions: Vec, + pub categories: PackageCategories, + pub mc_versions: Vec, pub downloads: usize, pub icon_url: String, pub date_created: DateTime, pub date_modified: DateTime, pub latest_version: String, - pub license: String, + pub license: Option, pub client_side: PackageSide, pub server_side: PackageSide, /// List of URLs to images pub gallery: Vec, - // pub color: i64, } #[onelauncher_macro::specta] @@ -87,7 +87,9 @@ pub struct ManagedPackage { pub package_type: PackageType, pub name: String, pub short_desc: String, - pub body: String, + pub body: ManagedPackageBody, + /// Won't have all versions for some providers, like CurseForge. + /// Try making a request to get the versions if needed pub version_ids: Vec, pub mc_versions: Vec, #[serde_as(as = "serde_with::VecSkipError<_>")] @@ -97,15 +99,36 @@ pub struct ManagedPackage { pub updated: DateTime, pub client: PackageSide, pub server: PackageSide, - pub categories: Vec, + pub categories: PackageCategories, pub license: Option, pub author: PackageAuthor, pub links: PackageLinks, pub status: PackageStatus, pub downloads: usize, + #[serde_as(as = "serde_with::VecSkipError<_>")] pub gallery: Vec, } +#[onelauncher_macro::specta] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ManagedPackageBody { + Url(String), + Raw(String), +} + +impl std::fmt::Display for ManagedPackageBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ManagedPackageBody::Url(_) => "Url", + ManagedPackageBody::Raw(_) => "Raw", + } + ) + } +} + #[onelauncher_macro::specta] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedUser { @@ -173,6 +196,25 @@ pub enum PackageAuthor { Users(Vec), } +impl std::fmt::Display for PackageAuthor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Team { team_id, org_id } => { + if let Some(org_id) = org_id { + format!("Team({} - {})", team_id, org_id) + } else { + format!("Team({})", team_id) + } + } + Self::Users(users) => format!("Users({})", users.len()), + } + ) + } +} + #[onelauncher_macro::specta] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] diff --git a/packages/core/src/api/packages/mod.rs b/packages/core/src/api/packages/mod.rs index 5a92fdc4..2eba3388 100644 --- a/packages/core/src/api/packages/mod.rs +++ b/packages/core/src/api/packages/mod.rs @@ -1,9 +1,10 @@ use data::{ManagedPackage, ManagedVersion}; use onelauncher_entity::icon::Icon; -use onelauncher_entity::package::PackageType; +use onelauncher_entity::package::{PackageType, Provider}; use onelauncher_entity::{clusters, packages}; use reqwest::Method; +use crate::api::packages::data::{ManagedPackageBody, PackageAuthor}; use crate::error::{LauncherError, LauncherResult}; use crate::store::Dirs; use crate::store::ingress::SubIngress; @@ -11,13 +12,11 @@ use crate::utils::crypto::HashAlgorithm; use crate::utils::io::IOError; use crate::utils::{http, icon, io}; +pub mod categories; pub mod dao; pub mod data; pub mod provider; -#[cfg(test)] -mod tests; - #[onelauncher_macro::error] #[derive(Debug, thiserror::Error)] pub enum PackageError { @@ -27,6 +26,12 @@ pub enum PackageError { IsModPack, #[error(transparent)] Incompatible(#[from] IncompatiblePackageType), + #[error("missing API key for provider '{0}'")] + MissingApiKey(Provider), + #[error("unsupported package body type '{0}'")] + UnsupportedBodyType(ManagedPackageBody), + #[error("unsupported author type '{0}'")] + UnsupportedAuthorType(PackageAuthor), } #[onelauncher_macro::error] @@ -200,5 +205,5 @@ fn join_package_file( version_id: &str, file_name: &str, ) -> std::path::PathBuf { - path.as_ref().join(format!("{version_id}-{file_name}")) + path.as_ref().join(version_id).join(file_name) } diff --git a/packages/core/src/api/packages/provider/curseforge/categories.rs b/packages/core/src/api/packages/provider/curseforge/categories.rs new file mode 100644 index 00000000..db66e914 --- /dev/null +++ b/packages/core/src/api/packages/provider/curseforge/categories.rs @@ -0,0 +1,103 @@ +use crate::api::packages::categories::{ + PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory, + ToProviderCategory, define_category_mapping, +}; +use crate::api::packages::provider::CurseForgeProviderImpl; + +pub struct CurseForgeCategories; + +define_category_mapping! { + impl ToProviderCategory for CurseForgeCategories { + PackageModCategory as as_mod_out => to_mod { + Adventure => 0, + Library => 0, + Equipment => 0, + Patches => 0, + Cosmetic => 0, + Food => 0, + Magic => 0, + Information => 0, + Misc => 0, + Performance => 0, + Redstone => 0, + ServerUtil => 0, + Storage => 0, + Technology => 0, + Farming => 0, + Automation => 0, + Transport => 0, + Utility => 0, + QoL => 0, + WorldGen => 0, + Mobs => 0, + Economy => 0, + Social => 0, + } + + PackageResourcePackCategory as as_resource_pack_out => to_resource_pack { + X8 => 0, + X16 => 0, + X32 => 0, + X48 => 0, + X64 => 0, + X128 => 0, + X256 => 0, + X512 => 0, + VanillaLike => 0, + Utility => 0, + Tweaks => 0, + Themed => 0, + Simplistic => 0, + Realistic => 0, + Modded => 0, + Decoration => 0, + Cursed => 0, + Combat => 0, + Audio => 0, + Blocks => 0, + CoreShaders => 0, + Gui => 0, + Fonts => 0, + Equipment => 0, + Environment => 0, + Entities => 0, + Items => 0, + Locale => 0, + Models => 0, + } + + PackageShaderCategory as as_shader_out => to_shader { + VanillaLike => 0, + SemiRealistic => 0, + Realistic => 0, + Fantasy => 0, + Cursed => 0, + Cartoon => 0, + Bloom => 0, + Atmosphere => 0, + Reflections => 0, + Shadows => 0, + PBR => 0, + PathTracing => 0, + Foliage => 0, + ColoredLightning => 0, + Potato => 0, + Low => 0, + Medium => 0, + High => 0, + Ultra => 0, + } + + PackageModPackCategory as as_mod_pack_out => to_mod_pack { + Adventure => 0, + Technology => 0, + Quests => 0, + Optimization => 0, + Multiplayer => 0, + Magic => 0, + LightWeight => 0, + Combat => 0, + Challenging => 0, + } + } +} diff --git a/packages/core/src/api/packages/provider/curseforge/mod.rs b/packages/core/src/api/packages/provider/curseforge/mod.rs new file mode 100644 index 00000000..2cbcf227 --- /dev/null +++ b/packages/core/src/api/packages/provider/curseforge/mod.rs @@ -0,0 +1,889 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use onelauncher_entity::loader::GameLoader; +use onelauncher_entity::package::{PackageType, Provider}; +use reqwest::Method; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_with::serde_as; +use url::Url; + +use crate::api::packages::PackageError; +use crate::api::packages::categories::ToProviderCategory; +use crate::api::packages::data::{ + ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, ManagedVersionDependency, + ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageGallery, PackageLinks, + PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, +}; +use crate::api::packages::provider::ProviderExt; +use crate::api::packages::provider::curseforge::categories::CurseForgeCategories; +use crate::error::LauncherResult; +use crate::store::Core; +use crate::utils::http; +use crate::utils::pagination::Paginated; + +#[derive(Default)] +pub struct CurseForgeProviderImpl; + +mod categories; + +#[async_trait::async_trait] +impl ProviderExt for CurseForgeProviderImpl { + async fn search(&self, filter: &SearchQuery) -> LauncherResult> { + let results: CFPaginatedData> = fetch_url_builder( + "/mods/search", + Some(|url: &mut Url| { + let mut params = url.query_pairs_mut(); + + // Always required parameters + params.append_pair("gameId", &crate::constants::CURSEFORGE_GAME_ID.to_string()); + + let package_type: CFPackageType = filter + .filters + .as_ref() + .and_then(|f| f.package_type.clone()) + .unwrap_or_default() + .into(); + params.append_pair("classId", &(package_type as u32).to_string()); + + params.append_pair("pageSize", &filter.limit.unwrap_or(10).to_string()); + params.append_pair("index", &filter.offset.unwrap_or(0).to_string()); + params.append_pair("sortField", &(SearchSort::Popularity as u8).to_string()); + params.append_pair("sortOrder", "desc"); + + // Optional: search query + if let Some(query) = &filter.query { + params.append_pair("searchFilter", query); + } + + if let Some(filters) = &filter.filters { + if let Some(game_versions) = &filters.game_versions { + // Need to create a list e.g. ["1.16.5", "1.17.1"] + let game_versions = format!( + "[{}]", + game_versions + .iter() + .map(|v| format!("\"{}\"", v)) + .collect::>() + .join(",") + ); + + params.append_pair("gameVersions", game_versions.as_str()); + } + + if let Some(loaders) = &filters.loaders { + // Need to create a list e.g. [1, 2, 3] + let loaders = format!( + "[{}]", + loaders + .iter() + .map(|l| (CFLoader::from(l.clone()) as u32).to_string()) + .collect::>() + .join(",") + ); + + params.append_pair("modLoaderTypes", loaders.as_str()); + } + + if let Some(categories) = &filters.categories { + let categories = CurseForgeCategories::as_out(categories); + + if !categories.is_empty() { + // Need to create a list e.g. [6, 12, 4471] + let categories = format!( + "[{}]", + categories + .iter() + .map(u32::to_string) + .collect::>() + .join(",") + ); + + params.append_pair("categoryIds", categories.as_str()); + } + } + } + }), + ) + .await?; + + Ok(Paginated { + total: results.pagination.total_count, + offset: results.pagination.index, + limit: results.pagination.page_size, + items: results.data.into_iter().map(SearchResult::from).collect(), + }) + } + + async fn get(&self, slug: &str) -> LauncherResult { + Ok(fetch::>(&format!("/mods/{slug}")) + .await? + .data + .into()) + } + + async fn get_multiple(&self, slugs: &[String]) -> LauncherResult> { + let body = serde_json::json!({ + "modIds": slugs.iter().filter_map(|s| s.parse::().ok()).collect::>(), + }); + + let results: CFData> = fetch_advanced( + Method::POST, + "/mods", + None::, + None, + Some(body), + ) + .await?; + + Ok(results.data.into_iter().map(ManagedPackage::from).collect()) + } + + async fn get_versions_by_hashes( + &self, + hashes: &[String], + ) -> LauncherResult> { + let body = serde_json::json!({ + "fingerprints": hashes.into_iter().filter_map(|hash| hash.parse::().ok()).collect::>() + }); + + #[derive(Deserialize)] + struct FingerprintMatch { + // id: u32, + file: CFModFile, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Response { + exact_matches: Vec, + // exact_fingerprints: Vec + } + + let results: CFData = fetch_advanced( + Method::POST, + &format!( + "/mods/fingerprints/{}", + crate::constants::CURSEFORGE_GAME_ID + ), + None::, + None, + Some(body), + ) + .await?; + + Ok(results + .data + .exact_matches + .into_iter() + .map(|file| { + ( + file.file.file_fingerprint.to_string(), + ManagedVersion::from(file.file), + ) + }) + .collect()) + } + + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult> { + let results = self.get_versions_by_hashes(&[hash.to_string()]).await?; + Ok(results.values().next().cloned()) + } + + async fn get_users_from_author( + &self, + author: PackageAuthor, + ) -> LauncherResult> { + match author { + PackageAuthor::Users(users) => Ok(users), + _ => Err(PackageError::UnsupportedAuthorType(author).into()), + } + } + + // async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + // todo!("CurseForge get users not implemented yet") + // } + + // async fn get_user(&self, slug: &str) -> LauncherResult { + // todo!("CurseForge get user not implemented yet") + // } + + async fn get_versions_paginated( + &self, + slug: &str, + mc_version: Option, + loaders: Option, + offset: usize, + limit: usize, + ) -> LauncherResult> { + let results: CFPaginatedData> = fetch_url_builder( + &format!("/mods/{slug}/files"), + Some(|url: &mut Url| { + let mut params = url.query_pairs_mut(); + + if let Some(mc_version) = mc_version { + params.append_pair("gameVersion", &mc_version); + } + + if let Some(loader) = loaders { + params + .append_pair("modLoaderType", &(CFLoader::from(loader) as u8).to_string()); + } + + let page = offset / limit; + + params.append_pair("pageSize", &limit.to_string()); + params.append_pair("index", &page.to_string()); + }), + ) + .await?; + + Ok(Paginated { + total: results.pagination.total_count, + offset: results.pagination.index, + limit: results.pagination.page_size, + items: results.data.into_iter().map(ManagedVersion::from).collect(), + }) + } + + async fn get_versions(&self, slugs: &[String]) -> LauncherResult> { + let json_body = serde_json::json!({ + "fileIds": slugs.iter().filter_map(|v| v.parse::().ok()).collect::>() + }); + + let results: CFData> = fetch_advanced( + Method::POST, + "/mods/files", + None::, + None, + Some(json_body), + ) + .await?; + + Ok(results.data.into_iter().map(ManagedVersion::from).collect()) + } + + async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult { + match body { + ManagedPackageBody::Raw(raw) => Ok(raw.clone()), + ManagedPackageBody::Url(url) => { + let body: String = fetch(url).await?; + Ok(body) + } + } + } +} + +// MARK: fetch helpers + +async fn fetch_advanced( + method: Method, + url: &str, + url_builder: Option, + headers: Option>, + json_body: Option, +) -> LauncherResult { + let core = Core::get(); + let key = core + .curseforge_api_key + .as_ref() + .ok_or(PackageError::MissingApiKey( + onelauncher_entity::package::Provider::CurseForge, + ))?; + + let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new()); + headers.insert("x-api-key", key); + + let url = &mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?; + + if let Some(url_builder) = url_builder { + url_builder(url); + } + + http::fetch_json_advanced(method, url.as_str(), json_body, Some(headers), None, None).await +} + +pub async fn fetch_url_builder( + url: &str, + url_builder: Option, +) -> LauncherResult { + fetch_advanced::(Method::GET, url, url_builder, None, None).await +} + +async fn fetch(url: &str) -> LauncherResult { + fetch_url_builder::(url, None).await +} + +// MARK: data structs +/// Mapping of Curseforge "classes" to their respective category ids +#[derive(Debug, Default, Serialize_repr, Deserialize_repr)] +#[repr(u32)] +pub enum CFPackageType { + #[default] + Mods = 6, + ModPack = 4471, + DataPack = 6945, + ResourcePack = 12, + ShaderPack = 6552, +} + +impl From for PackageType { + fn from(package_type: CFPackageType) -> Self { + match package_type { + CFPackageType::Mods => Self::Mod, + CFPackageType::ModPack => Self::ModPack, + CFPackageType::DataPack => Self::DataPack, + CFPackageType::ResourcePack => Self::ResourcePack, + CFPackageType::ShaderPack => Self::Shader, + } + } +} + +impl From for CFPackageType { + fn from(package_type: PackageType) -> Self { + match package_type { + PackageType::Mod => Self::Mods, + PackageType::ModPack => Self::ModPack, + PackageType::DataPack => Self::DataPack, + PackageType::ResourcePack => Self::ResourcePack, + PackageType::Shader => Self::ShaderPack, + } + } +} + +/// Mapping of Curseforge supported loaders to their respective id +#[derive(Debug, Default, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum CFLoader { + #[default] + Any = 0, + Forge = 1, + Cauldron = 2, + LiteLoader = 3, + Fabric = 4, + Quilt = 5, + NeoForge = 6, +} + +impl From for CFLoader { + fn from(loader: String) -> Self { + match loader.to_lowercase().as_str() { + "forge" => CFLoader::Forge, + "cauldron" => CFLoader::Cauldron, + "liteloader" => CFLoader::LiteLoader, + "fabric" => CFLoader::Fabric, + "quilt" => CFLoader::Quilt, + "neoforge" => CFLoader::NeoForge, + _ => CFLoader::Any, + } + } +} + +impl From for CFLoader { + fn from(loader: GameLoader) -> Self { + match loader { + GameLoader::Forge => CFLoader::Forge, + GameLoader::Fabric | GameLoader::LegacyFabric => CFLoader::Fabric, + GameLoader::Quilt => CFLoader::Quilt, + GameLoader::NeoForge => CFLoader::NeoForge, + _ => CFLoader::Any, + } + } +} + +impl From for GameLoader { + fn from(loader: CFLoader) -> Self { + match loader { + CFLoader::Forge => Self::Forge, + CFLoader::Fabric => Self::Fabric, + CFLoader::Quilt => Self::Quilt, + CFLoader::NeoForge => Self::NeoForge, + _ => GameLoader::Vanilla, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde_as] +#[serde(rename_all = "camelCase")] +pub struct CFPackage { + pub id: usize, + pub game_id: usize, + #[serde(alias = "classId")] + pub package_type: CFPackageType, + pub name: String, + pub slug: String, + pub links: CFLinks, + #[serde(default)] + #[serde_as(as = "serde_with::VecSkipError<_>")] + pub social_links: Vec, + pub summary: String, + pub status: CFStatus, + pub download_count: usize, + #[serde(default)] + pub is_featured: bool, + pub categories: Vec, + pub authors: Vec, + pub logo: Option, + pub screenshots: Vec, + #[serde_as(as = "serde_with::VecSkipError<_>")] + pub latest_files_indexes: Vec, + pub date_created: DateTime, + pub date_modified: DateTime, + pub date_released: DateTime, + pub rating: Option, +} + +impl From for ManagedPackage { + fn from(value: CFPackage) -> Self { + let package_type: PackageType = value.package_type.into(); + + let mut loaders: Vec = Vec::new(); + let mut mc_versions: Vec = Vec::new(); + let mut latest_version_ids: Vec = Vec::new(); + + for index in value.latest_files_indexes { + mc_versions.push(index.game_version); + latest_version_ids.push(index.file_id.to_string()); + + if let Some(loader) = index + .mod_loader + .and_then(|loader| GameLoader::try_from(loader).ok()) + { + loaders.push(loader); + } + } + + Self { + provider: Provider::CurseForge, + id: value.id.to_string(), + slug: value.slug, + name: value.name, + short_desc: value.summary, + body: ManagedPackageBody::Url(format!("/v1/mods/{}/description", value.id)), + version_ids: latest_version_ids, + mc_versions, + loaders, + icon_url: value.logo.map(|logo| logo.url), + created: value.date_created, + updated: value.date_modified, + client: PackageSide::Unknown, // TODO: determine client side + server: PackageSide::Unknown, // TODO: determine server side + categories: CurseForgeCategories::to_list( + &package_type, + &value.categories.into_iter().map(|c| c.id).collect(), + ), + package_type, + license: None, + author: PackageAuthor::Users( + value + .authors + .into_iter() + .map(|author| ManagedUser { + id: author.id.to_string(), + username: author.name, + url: Some(author.url), + is_organization_user: false, + avatar_url: author.avatar_url, + bio: None, + role: None, + }) + .collect(), + ), + links: PackageLinks { + source: value.links.source_url, + issues: value.links.issues_url, + wiki: value.links.wiki_url, + donation: None, // we love curseforge + + discord: value + .social_links + .iter() + .find(|l| l.link_type == CFSocialLinkType::Discord) + .map(|l| l.url.clone()), + website: value + .social_links + .iter() + .find(|l| l.link_type == CFSocialLinkType::Website) + .map(|l| l.url.clone()), + + ..Default::default() + }, + status: value.status.into(), + downloads: value.download_count, + gallery: value.screenshots.into_iter().map(Into::into).collect(), + } + } +} + +impl From for SearchResult { + fn from(value: CFPackage) -> Self { + let mc_versions: Vec = value + .latest_files_indexes + .into_iter() + .map(|index| index.game_version) + .collect(); + + let package_type: PackageType = value.package_type.into(); + + Self { + title: value.name, + slug: value.slug, + project_id: value.id.to_string(), + author: value + .authors + .first() + .and_then(|a| Some(a.name.clone())) + .unwrap_or(String::from("Unknown")), + description: value.summary, + client_side: PackageSide::Unknown, + server_side: PackageSide::Unknown, + downloads: value.download_count, + icon_url: value.logo.map_or(String::new(), |l| l.url), + categories: CurseForgeCategories::to_list( + &package_type, + &value.categories.into_iter().map(|c| c.id).collect(), + ), + package_type, + mc_versions, + date_created: value.date_created, + date_modified: value.date_modified, + gallery: value + .screenshots + .into_iter() + .map(|asset| asset.url) + .collect(), + latest_version: "".to_string(), // TODO: figure out how to get this + license: None, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFLatestFileIndex { + pub game_version: String, + pub file_id: usize, + #[serde(rename = "filename")] + pub file_name: String, + // pub release_type: ModFileReleaseType, + pub mod_loader: Option, +} + +#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)] +#[repr(u64)] +pub enum CFStatus { + #[default] + Unknown = 0, + New = 1, + Approved = 4, + Rejected = 5, + Deleted = 9, +} + +impl From for PackageStatus { + fn from(status: CFStatus) -> Self { + match status { + CFStatus::Unknown => Self::Active, + CFStatus::New => Self::Active, + CFStatus::Approved => Self::Active, + CFStatus::Rejected => Self::Abandoned, + CFStatus::Deleted => Self::Abandoned, + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFLinks { + pub website_url: Option, + pub wiki_url: Option, + pub issues_url: Option, + pub source_url: Option, +} + +// undocumented ?? +#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)] +#[repr(u64)] +pub enum CFSocialLinkType { + None = 0, + Mastodon = 1, + Discord = 2, + Website = 3, + Facebook = 4, + Twitter = 5, + Instagram = 6, + Patreon = 7, + Twitch = 8, + Reddit = 9, + Youtube = 10, + TikTok = 11, + Pinterest = 12, + Github = 13, + BlueSky = 14, + + #[default] + #[serde(other)] + Unknown, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFSocialLink { + #[serde(rename = "type")] + pub link_type: CFSocialLinkType, + pub url: String, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Category { + pub id: u32, + // pub game_id: u32, + // pub name: String, + pub slug: String, + // pub url: String, + // pub icon_url: String, + // pub date_modified: String, + // pub parent_category_id: u32, + // pub display_index: u32, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFAuthor { + pub id: u32, + pub name: String, + pub url: String, + pub avatar_url: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFModAsset { + pub id: u32, + pub mod_id: u32, + pub title: String, + pub description: String, + pub thumbnail_url: String, + pub url: String, +} + +impl From for PackageGallery { + fn from(screenshot: CFModAsset) -> Self { + Self { + url: screenshot.url, + thumbnail_url: screenshot.thumbnail_url, + title: Some(screenshot.title), + description: Some(screenshot.description), + featured: None, + } + } +} + +#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)] +#[repr(u8)] +pub enum ModFileReleaseType { + #[default] + Release = 1, + Beta = 2, + Alpha = 3, +} + +impl From for PackageReleaseType { + fn from(value: ModFileReleaseType) -> Self { + match value { + ModFileReleaseType::Release => Self::Release, + ModFileReleaseType::Beta => Self::Beta, + ModFileReleaseType::Alpha => Self::Alpha, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFModFile { + pub id: u32, + pub game_id: u32, + pub mod_id: u32, + #[serde(default)] + pub is_available: bool, + pub display_name: String, + pub file_name: String, + pub release_type: ModFileReleaseType, + // pub file_status: u32, + pub hashes: Vec, + pub file_date: DateTime, + pub file_length: usize, + pub download_count: usize, + pub file_size_on_disk: Option, + pub download_url: String, + pub game_versions: Vec, + pub sortable_game_versions: Vec, + pub dependencies: Vec, + // pub expose_as_alternative: bool, + // pub parent_project_file_id: u32, + // pub alternate_file_id: u32, + // pub is_server_pack: bool, + // pub server_pack_file_id: u32, + // pub is_early_access_content: bool, + // pub early_access_end_date: String, + pub file_fingerprint: u32, + // pub modules: Vec, +} + +impl From for ManagedVersion { + fn from(value: CFModFile) -> ManagedVersion { + let mut hashes = HashMap::new(); + + for hash in value.hashes { + hashes.insert(hash.algo.name(), hash.value); + } + + let mut mc_versions = Vec::new(); + let mut loaders = Vec::new(); + + for version in value.sortable_game_versions { + if version.game_version.is_empty() { + // This is most likely a loader + loaders.push(CFLoader::from(version.game_version_name)); + } else { + mc_versions.push(version.game_version); + } + } + + let mut files = Vec::new(); + files.push(ManagedVersionFile { + url: value.download_url, + file_name: value.file_name.clone(), + primary: true, + size: value.file_size_on_disk.unwrap_or(value.file_length), + sha1: hashes.get("sha1").cloned().unwrap_or_default(), + }); + + ManagedVersion { + project_id: value.mod_id.to_string(), + version_id: value.id.to_string(), + display_name: value.display_name.clone(), + loaders: loaders.into_iter().map(Into::into).collect(), + mc_versions, + changelog: None, + display_version: value.file_name, + release_type: value.release_type.into(), + downloads: value.download_count, + published: value.file_date, + dependencies: value.dependencies.into_iter().map(Into::into).collect(), + files, + } + } +} + +#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] +#[repr(u8)] +pub enum HashAlgorithm { + Sha1 = 1, + MD5 = 2, +} + +impl HashAlgorithm { + pub fn name(&self) -> String { + match self { + Self::Sha1 => "sha1", + Self::MD5 => "md5", + } + .to_string() + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Hash { + pub value: String, + pub algo: HashAlgorithm, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SortableGameVersion { + pub game_version_name: String, + pub game_version_padded: String, + pub game_version: String, + pub game_version_release_date: String, + pub game_version_type_id: u32, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CFDependency { + pub mod_id: usize, + pub relation_type: CFRelationType, +} + +impl From for ManagedVersionDependency { + fn from(value: CFDependency) -> Self { + Self { + project_id: Some(value.mod_id.to_string()), + dependency_type: value.relation_type.into(), + file_name: None, + version_id: None, + } + } +} + +#[derive(Default, Debug, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum CFRelationType { + Embedded = 1, + #[default] + Optional = 2, + Required = 3, + Tool = 4, + Incompatible = 5, + Include = 6, +} + +impl From for PackageDependencyType { + fn from(value: CFRelationType) -> Self { + match value { + CFRelationType::Embedded => Self::Embedded, + CFRelationType::Optional => Self::Optional, + CFRelationType::Required => Self::Required, + CFRelationType::Tool => Self::Optional, + CFRelationType::Incompatible => Self::Incompatible, + CFRelationType::Include => Self::Embedded, + } + } +} + +// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField +#[allow(dead_code)] +enum SearchSort { + Featured = 1, + Popularity = 2, + LastUpdated = 3, + Name = 4, + Author = 5, + TotalDownloads = 6, + Rating = 12, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +struct CFData { + data: T, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +struct CFPaginatedData { + pagination: Pagination, + data: T, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Pagination { + pub index: usize, + pub page_size: usize, + pub total_count: usize, +} diff --git a/packages/core/src/api/packages/provider/mod.rs b/packages/core/src/api/packages/provider/mod.rs index 201be575..1aad2d1e 100644 --- a/packages/core/src/api/packages/provider/mod.rs +++ b/packages/core/src/api/packages/provider/mod.rs @@ -1,15 +1,25 @@ +use std::collections::HashMap; + use onelauncher_entity::loader::GameLoader; use onelauncher_entity::package::Provider; -use crate::api::packages::data::SearchResult; +use crate::api::packages::PackageError; +use crate::api::packages::data::{ManagedPackageBody, SearchResult}; use crate::error::LauncherResult; use crate::utils::pagination::Paginated; use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery}; -mod modrinth; +#[cfg(test)] +mod tests; +mod curseforge; +mod modrinth; +// mod skyclient; + +pub use curseforge::CurseForgeProviderImpl; pub use modrinth::ModrinthProviderImpl; +// pub use skyclient::SkyClientProviderImpl; #[async_trait::async_trait] pub trait ProviderExt { @@ -19,8 +29,8 @@ pub trait ProviderExt { async fn get_versions_by_hashes( &self, hashes: &[String], - ) -> LauncherResult>; - async fn get_version_by_hash(&self, hash: &str) -> LauncherResult; + ) -> LauncherResult>; + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult>; // async fn get_org_projects(&self, slug: &str) -> LauncherResult>; @@ -28,107 +38,96 @@ pub trait ProviderExt { &self, author: PackageAuthor, ) -> LauncherResult>; - async fn get_users(&self, slugs: &[String]) -> LauncherResult>; - async fn get_user(&self, slug: &str) -> LauncherResult; + // async fn get_users(&self, slugs: &[String]) -> LauncherResult>; + // async fn get_user(&self, slug: &str) -> LauncherResult; async fn get_versions_paginated( &self, slug: &str, - mc_versions: Option>, - loaders: Option>, + mc_version: Option, + loader: Option, offset: usize, limit: usize, ) -> LauncherResult>; async fn get_versions(&self, slugs: &[String]) -> LauncherResult>; + + async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult { + match body { + ManagedPackageBody::Raw(raw) => Ok(raw.clone()), + _ => Err(PackageError::UnsupportedBodyType(body.clone()).into()), + } + } } +/// Cleans up code duplication by delegating method calls to the appropriate provider implementation. +macro_rules! delegate { + ($self:expr, $method:ident($($arg:expr),*)) => { + match $self { + Provider::Modrinth => ModrinthProviderImpl.$method($($arg),*).await, + Provider::CurseForge => CurseForgeProviderImpl.$method($($arg),*).await, + // Provider::SkyClient => SkyClientProviderImpl.$method($($arg),*).await, + _ => todo!("skyclient not implement") + } + }; +} + +/// Wrapper around all supported providers into a single enum. #[async_trait::async_trait] impl ProviderExt for Provider { async fn search(&self, query: &SearchQuery) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.search(query).await, - _ => todo!("unimplemented provider"), - } + delegate!(self, search(query)) } async fn get(&self, slug: &str) -> LauncherResult { - match self { - Self::Modrinth => ModrinthProviderImpl.get(slug).await, - _ => todo!("unimplemented provider"), - } + delegate!(self, get(slug)) } async fn get_multiple(&self, slugs: &[String]) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.get_multiple(slugs).await, - _ => todo!("unimplemented provider"), - } + delegate!(self, get_multiple(slugs)) } async fn get_versions_by_hashes( &self, hashes: &[String], - ) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.get_versions_by_hashes(hashes).await, - _ => todo!("unimplemented provider"), - } + ) -> LauncherResult> { + delegate!(self, get_versions_by_hashes(hashes)) } - async fn get_version_by_hash(&self, hash: &str) -> LauncherResult { - match self { - Self::Modrinth => ModrinthProviderImpl.get_version_by_hash(hash).await, - _ => todo!("unimplemented provider"), - } + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult> { + delegate!(self, get_version_by_hash(hash)) } async fn get_users_from_author( &self, author: PackageAuthor, ) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.get_users_from_author(author).await, - _ => todo!("unimplemented provider"), - } + delegate!(self, get_users_from_author(author)) } - async fn get_users(&self, slugs: &[String]) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.get_users(slugs).await, - _ => todo!("unimplemented provider"), - } - } + // async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + // delegate!(self, get_users(slugs)) + // } - async fn get_user(&self, slug: &str) -> LauncherResult { - match self { - Self::Modrinth => ModrinthProviderImpl.get_user(slug).await, - _ => todo!("unimplemented provider"), - } - } + // async fn get_user(&self, slug: &str) -> LauncherResult { + // delegate!(self, get_user(slug)) + // } async fn get_versions_paginated( &self, slug: &str, - mc_versions: Option>, - loaders: Option>, + mc_version: Option, + loader: Option, offset: usize, limit: usize, ) -> LauncherResult> { - match self { - Self::Modrinth => { - ModrinthProviderImpl - .get_versions_paginated(slug, mc_versions, loaders, offset, limit) - .await - } - _ => todo!("unimplemented provider"), - } + delegate!( + self, + get_versions_paginated(slug, mc_version, loader, offset, limit) + ) } async fn get_versions(&self, slugs: &[String]) -> LauncherResult> { - match self { - Self::Modrinth => ModrinthProviderImpl.get_versions(slugs).await, - _ => todo!("unimplemented provider"), - } + delegate!(self, get_versions(slugs)) } } diff --git a/packages/core/src/api/packages/provider/modrinth/categories.rs b/packages/core/src/api/packages/provider/modrinth/categories.rs new file mode 100644 index 00000000..1ff0f3c0 --- /dev/null +++ b/packages/core/src/api/packages/provider/modrinth/categories.rs @@ -0,0 +1,103 @@ +use crate::api::packages::categories::{ + PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory, + ToProviderCategory, define_category_mapping, +}; +use crate::api::packages::provider::ModrinthProviderImpl; + +pub struct ModrinthCategories; + +define_category_mapping! { + impl ToProviderCategory for ModrinthCategories { + PackageModCategory as as_mod_out => to_mod { + Adventure => String::from("adventure"), + Library => String::from("library"), + Equipment => String::from("equipment"), + Patches => String::from("optimization"), + Cosmetic => String::from("decoration"), + Food => String::from("food"), + Magic => String::from("magic"), + Information => String::from("utility"), + Misc => String::from("utility"), + Performance => String::from("optimization"), + Redstone => String::from("technology"), + ServerUtil => String::from("management"), + Storage => String::from("storage"), + Technology => String::from("technology"), + Farming => String::from("food"), + Automation => String::from("technology"), + Transport => String::from("transportation"), + Utility => String::from("utility"), + QoL => String::from("utility"), + WorldGen => String::from("worldgen"), + Mobs => String::from("mobs"), + Economy => String::from("economy"), + Social => String::from("social"), + } + + PackageResourcePackCategory as as_resource_pack_out => to_resource_pack { + X8 => String::from("8x-"), + X16 => String::from("16x"), + X32 => String::from("32x"), + X48 => String::from("48x"), + X64 => String::from("64x"), + X128 => String::from("128x"), + X256 => String::from("256x"), + X512 => String::from("512x+"), + VanillaLike => String::from("vanilla-like"), + Utility => String::from("utility"), + Tweaks => String::from("tweaks"), + Themed => String::from("themed"), + Simplistic => String::from("simplistic"), + Realistic => String::from("realistic"), + Modded => String::from("modded"), + Decoration => String::from("decoration"), + Cursed => String::from("cursed"), + Combat => String::from("combat"), + Audio => String::from("audio"), + Blocks => String::from("blocks"), + CoreShaders => String::from("core-shaders"), + Gui => String::from("gui"), + Fonts => String::from("fonts"), + Equipment => String::from("equipment"), + Environment => String::from("environment"), + Entities => String::from("entities"), + Items => String::from("items"), + Locale => String::from("locale"), + Models => String::from("models"), + } + + PackageShaderCategory as as_shader_out => to_shader { + VanillaLike => String::from("vanilla-like"), + SemiRealistic => String::from("semi-realistic"), + Realistic => String::from("realistic"), + Fantasy => String::from("fantasy"), + Cursed => String::from("cursed"), + Cartoon => String::from("cartoon"), + Bloom => String::from("bloom"), + Atmosphere => String::from("atmosphere"), + Reflections => String::from("reflections"), + Shadows => String::from("shadows"), + PBR => String::from("pbr"), + PathTracing => String::from("path-tracing"), + Foliage => String::from("foliage"), + ColoredLightning => String::from("colored-lightning"), + Potato => String::from("potato"), + Low => String::from("low"), + Medium => String::from("medium"), + High => String::from("high"), + Ultra => String::from("screenshot"), + } + + PackageModPackCategory as as_mod_pack_out => to_mod_pack { + Adventure => String::from("adventure"), + Technology => String::from("technology"), + Quests => String::from("quests"), + Optimization => String::from("optimization"), + Multiplayer => String::from("multiplayer"), + Magic => String::from("magic"), + LightWeight => String::from("lightweight"), + Combat => String::from("combat"), + Challenging => String::from("challenging"), + } + } +} diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth/mod.rs similarity index 77% rename from packages/core/src/api/packages/provider/modrinth.rs rename to packages/core/src/api/packages/provider/modrinth/mod.rs index cf8673c7..12af9321 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth/mod.rs @@ -1,10 +1,14 @@ +use std::collections::HashMap; + use super::ProviderExt; +use crate::api::packages::categories::ToProviderCategory; use crate::api::packages::data::{ - DEFAULT_LIMIT, ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, - ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, - PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, - SearchResult, + DEFAULT_LIMIT, ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, + ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, + PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType, + PackageSide, PackageStatus, SearchQuery, SearchResult, }; +use crate::api::packages::provider::modrinth::categories::ModrinthCategories; use crate::error::LauncherResult; use crate::utils::http; use crate::utils::pagination::Paginated; @@ -16,6 +20,8 @@ use serde::Deserialize; use serde_with::serde_as; use url::Url; +mod categories; + macro_rules! url { ($($arg:tt)*) => { format!("{}/v2{}", crate::constants::MODRINTH_API_URL, format!($($arg)*)).as_str() @@ -31,13 +37,6 @@ macro_rules! url_v3 { #[derive(Default)] pub struct ModrinthProviderImpl; -impl ModrinthProviderImpl { - #[must_use] - pub const fn new() -> Self { - Self - } -} - #[async_trait::async_trait] impl ProviderExt for ModrinthProviderImpl { async fn search(&self, query: &SearchQuery) -> LauncherResult> { @@ -54,39 +53,33 @@ impl ProviderExt for ModrinthProviderImpl { let mut builder = FacetBuilder::builder(); if let Some(categories) = &filters.categories { - for category in categories { - builder.and(Facet( - "categories".to_string(), - FacetOperation::Eq, - category.to_string(), - )); + for category in ModrinthCategories::as_out(categories) { + builder.and(Facet::new("categories", FacetOperation::Eq, category)); } } if let Some(mc_versions) = &filters.game_versions { for mc_version in mc_versions { - builder.and(Facet( - "versions".to_string(), + builder.and(Facet::new( + "versions", FacetOperation::Eq, - mc_version.to_string(), + mc_version.clone(), )); } } - if let Some(package_types) = &filters.package_types { - for package_type in package_types { - builder.and(Facet( - "project_type".to_string(), - FacetOperation::Eq, - package_type_to_string(package_type), - )); - } + if let Some(package_type) = &filters.package_type { + builder.and(Facet::new( + "project_type", + FacetOperation::Eq, + package_type.to_string(), + )); } if let Some(loaders) = &filters.loaders { for loader in loaders { - builder.and(Facet( - "categories".to_string(), + builder.and(Facet::new( + "categories", FacetOperation::Eq, loader.to_string(), )); @@ -152,33 +145,34 @@ impl ProviderExt for ModrinthProviderImpl { async fn get_versions_by_hashes( &self, hashes: &[String], - ) -> LauncherResult> { + ) -> LauncherResult> { let body = serde_json::json!({ "hashes": hashes, "algorithm": "sha1" }); - Ok(http::fetch_json::>( + let results = http::fetch_json::>( Method::POST, url!("/version_files"), Some(body), None, ) - .await? - .into_iter() - .map(Into::into) - .collect()) + .await?; + + Ok(results.into_iter().map(|(k, v)| (k, v.into())).collect()) } - async fn get_version_by_hash(&self, hash: &str) -> LauncherResult { - Ok(http::fetch_json::( - Method::GET, - url!("/version_file/{hash}"), - None, - None, - ) - .await? - .into()) + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult> { + Ok(Some( + http::fetch_json::( + Method::GET, + url!("/version_file/{hash}"), + None, + None, + ) + .await? + .into(), + )) } // async fn get_org_projects(&self, slug: &str) -> LauncherResult> { @@ -252,45 +246,45 @@ impl ProviderExt for ModrinthProviderImpl { Ok(users) } - async fn get_users(&self, slugs: &[String]) -> LauncherResult> { - Ok(http::fetch_json::>( - Method::GET, - url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?), - None, - None, - ) - .await? - .into_iter() - .map(Into::into) - .collect()) - } + // async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + // Ok(http::fetch_json::>( + // Method::GET, + // url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?), + // None, + // None, + // ) + // .await? + // .into_iter() + // .map(Into::into) + // .collect()) + // } - async fn get_user(&self, slug: &str) -> LauncherResult { - Ok( - http::fetch_json::(Method::GET, url!("/user/{slug}"), None, None) - .await? - .into(), - ) - } + // async fn get_user(&self, slug: &str) -> LauncherResult { + // Ok( + // http::fetch_json::(Method::GET, url!("/user/{slug}"), None, None) + // .await? + // .into(), + // ) + // } async fn get_versions_paginated( &self, slug: &str, - mc_versions: Option>, - loaders: Option>, + mc_version: Option, + loader: Option, offset: usize, limit: usize, ) -> LauncherResult> { let mut url = Url::parse(url!("/project/{slug}/version"))?; - if let Some(mc_versions) = mc_versions { + if let Some(mc_version) = mc_version { url.query_pairs_mut() - .append_pair("game_versions", &serde_json::to_string(&mc_versions)?); + .append_pair("game_versions", &format!("[\"{}\"]", mc_version)); } - if let Some(loaders) = loaders { + if let Some(loader) = loader { url.query_pairs_mut() - .append_pair("loaders", &serde_json::to_string(&loaders)?); + .append_pair("loaders", &format!("[\"{}\"]", loader.to_string())); } // url.query_pairs_mut() @@ -335,15 +329,50 @@ impl ProviderExt for ModrinthProviderImpl { } } -fn package_type_to_string(package_type: &PackageType) -> String { - match package_type { - PackageType::Mod => "mod", - PackageType::ResourcePack => "resourcepack", - PackageType::Shader => "shader", - PackageType::DataPack => "datapack", - PackageType::ModPack => "modpack", +#[derive(Deserialize)] +struct ModrinthSearchResult { + pub project_id: String, + pub project_type: PackageType, + pub slug: String, + pub author: String, + pub title: String, + pub description: String, + pub categories: Vec, + pub mc_versions: Vec, + pub downloads: usize, + pub icon_url: String, + pub date_created: DateTime, + pub date_modified: DateTime, + pub latest_version: String, + pub license: Option, + pub client_side: PackageSide, + pub server_side: PackageSide, + /// List of URLs to images + pub gallery: Vec, +} + +impl From for SearchResult { + fn from(value: ModrinthSearchResult) -> Self { + Self { + categories: ModrinthCategories::to_list(&value.project_type, &value.categories), + package_type: value.project_type, + project_id: value.project_id, + slug: value.slug, + author: value.author, + title: value.title, + description: value.description, + mc_versions: value.mc_versions, + downloads: value.downloads, + icon_url: value.icon_url, + date_created: value.date_created, + date_modified: value.date_modified, + latest_version: value.latest_version, + license: value.license, + client_side: value.client_side, + server_side: value.server_side, + gallery: value.gallery, + } } - .to_string() } #[serde_as] @@ -407,10 +436,9 @@ impl From for ManagedPackage { provider: Provider::Modrinth, id: value.id, slug: value.slug, - package_type: value.project_type, name: value.title, short_desc: value.description, - body: value.body, + body: ManagedPackageBody::Raw(value.body), version_ids: value.versions, mc_versions: value.game_versions, loaders: value.loaders, @@ -419,7 +447,8 @@ impl From for ManagedPackage { updated: value.updated, client: value.client_side, server: value.server_side, - categories: value.categories, + categories: ModrinthCategories::to_list(&value.project_type, &value.categories), + package_type: value.project_type, license: value.license, author: PackageAuthor::Team { team_id: value.team, @@ -444,20 +473,25 @@ impl From for ManagedPackage { } } +#[serde_as] #[derive(Deserialize)] struct ModrinthVersion { pub name: String, pub version_number: String, pub changelog: Option, + #[serde_as(as = "serde_with::VecSkipError<_>")] pub dependencies: Vec, + #[serde_as(as = "serde_with::VecSkipError<_>")] pub game_versions: Vec, pub version_type: PackageReleaseType, + #[serde_as(as = "serde_with::VecSkipError<_>")] pub loaders: Vec, // pub featured: bool, pub id: String, pub project_id: String, pub date_published: DateTime, pub downloads: usize, + #[serde_as(as = "serde_with::VecSkipError<_>")] pub files: Vec, } @@ -611,6 +645,12 @@ impl std::fmt::Display for FacetOperation { struct Facet(pub String, pub FacetOperation, pub String); +impl Facet { + pub fn new(key: &str, operation: FacetOperation, value: String) -> Self { + Self(key.to_string(), operation, value) + } +} + impl std::fmt::Display for Facet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str()) diff --git a/packages/core/src/api/packages/provider/skyclient.rs b/packages/core/src/api/packages/provider/skyclient.rs new file mode 100644 index 00000000..f974f231 --- /dev/null +++ b/packages/core/src/api/packages/provider/skyclient.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use onelauncher_entity::loader::GameLoader; + +use crate::api::packages::data::{ + ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult, +}; +use crate::api::packages::provider::ProviderExt; +use crate::error::LauncherResult; +use crate::utils::pagination::Paginated; + +#[derive(Default)] +pub struct SkyClientProviderImpl; + +#[async_trait::async_trait] +impl ProviderExt for SkyClientProviderImpl { + async fn search(&self, query: &SearchQuery) -> LauncherResult> { + todo!("SkyClient search not implemented yet") + } + + async fn get(&self, slug: &str) -> LauncherResult { + todo!("SkyClient get package not implemented yet") + } + + async fn get_multiple(&self, slugs: &[String]) -> LauncherResult> { + todo!("SkyClient get multiple packages not implemented yet") + } + + async fn get_versions_by_hashes( + &self, + hashes: &[String], + ) -> LauncherResult> { + todo!("SkyClient get versions by hashes not implemented yet") + } + + async fn get_version_by_hash(&self, hash: &str) -> LauncherResult> { + todo!("SkyClient get version by hash not implemented yet") + } + + async fn get_users_from_author( + &self, + author: PackageAuthor, + ) -> LauncherResult> { + todo!("SkyClient get users from author not implemented yet") + } + + // async fn get_users(&self, slugs: &[String]) -> LauncherResult> { + // todo!("SkyClient get users not implemented yet") + // } + + // async fn get_user(&self, slug: &str) -> LauncherResult { + // todo!("SkyClient get user not implemented yet") + // } + + async fn get_versions_paginated( + &self, + slug: &str, + mc_version: Option, + loader: Option, + offset: usize, + limit: usize, + ) -> LauncherResult> { + todo!("SkyClient get versions paginated not implemented yet") + } + + async fn get_versions(&self, slugs: &[String]) -> LauncherResult> { + todo!("SkyClient get versions not implemented yet") + } +} diff --git a/packages/core/src/api/packages/provider/tests.rs b/packages/core/src/api/packages/provider/tests.rs new file mode 100644 index 00000000..d080ac32 --- /dev/null +++ b/packages/core/src/api/packages/provider/tests.rs @@ -0,0 +1,211 @@ +use std::sync::LazyLock; + +use onelauncher_entity::package::{PackageType, Provider}; + +use crate::api::packages; +use crate::api::packages::provider::ProviderExt; +use crate::error::LauncherResult; +use crate::store::{Core, CoreOptions, Dirs}; + +macro_rules! init { + () => {{ + Core::initialize(CoreOptions::default()).await?; + if Core::get().curseforge_api_key.is_none() { + eprintln!("CURSEFORGE_API_KEY is not set, skipping tests that require it."); + false + } else { + true + } + }}; +} + +struct Entry { + slug: &'static str, + id: &'static str, + version_id: &'static str, + package_type: PackageType, +} + +static DATA: LazyLock)>> = LazyLock::new(|| { + vec![ + ( + Provider::CurseForge, + vec![ + Entry { + slug: "oneconfig", + id: "1148006", + version_id: "5938441", + package_type: PackageType::Mod, + }, + Entry { + slug: "complementary-reimagined", + id: "627557", + version_id: "6515577", + package_type: PackageType::Shader, + }, + Entry { + slug: "tschipcrafts-dynamic-lights", + id: "831385", + version_id: "6725286", + package_type: PackageType::DataPack, + }, + Entry { + slug: "fresh-animations", + id: "453763", + version_id: "6528594", + package_type: PackageType::ResourcePack, + }, + Entry { + slug: "all-the-mods-10", + id: "925200", + version_id: "6826503", + package_type: PackageType::ModPack, + }, + ], + ), + ( + Provider::Modrinth, + vec![ + Entry { + slug: "oneconfig", + id: "AibBIVmj", + version_id: "YofF8Rpk", + package_type: PackageType::Mod, + }, + Entry { + slug: "complementary-reimagined", + id: "HVnmMxH1", + version_id: "sAAjYvFB", + package_type: PackageType::Shader, + }, + Entry { + slug: "veinminer", + id: "OhduvhIc", + version_id: "gZ4v72II", + package_type: PackageType::Mod, + }, + Entry { + slug: "fresh-animations", + id: "50dA9Sha", + version_id: "9LtDLleW", + package_type: PackageType::ResourcePack, + }, + Entry { + slug: "fabulously-optimized", + id: "1KVo5zza", + version_id: "U9MwqSo1", + package_type: PackageType::ModPack, + }, + ], + ), + ] +}); + +#[tokio::test] +pub async fn test_get() -> LauncherResult<()> { + let cf = init!(); + + for (provider, entries) in DATA.iter() { + if provider == &Provider::CurseForge && !cf { + continue; + } + + for entry in entries { + let res = provider.get(entry.id).await?; + + assert_eq!(res.slug, entry.slug); + assert_eq!(res.id, entry.id); + assert_eq!(res.package_type, entry.package_type); + } + } + + Ok(()) +} + +#[tokio::test] +pub async fn test_get_multiple() -> LauncherResult<()> { + let cf = init!(); + + for (provider, entries) in DATA.iter() { + if provider == &Provider::CurseForge && !cf { + continue; + } + + let ids = entries + .iter() + .map(|e| e.id.to_string()) + .collect::>(); + let res = provider.get_multiple(&ids).await?; + + assert_eq!(res.len(), entries.len()); + } + + Ok(()) +} + +#[tokio::test] +pub async fn test_get_versions() -> LauncherResult<()> { + let cf = init!(); + + for (provider, entries) in DATA.iter() { + if provider == &Provider::CurseForge && !cf { + continue; + } + + let slugs = entries + .iter() + .map(|e| e.version_id.to_string()) + .collect::>(); + + let res = provider + .get_versions(&slugs.iter().map(|s| s.to_string()).collect::>()) + .await?; + + assert_eq!(res.len(), slugs.len()); + } + + Ok(()) +} + +#[tokio::test] +pub async fn test_download_packages() -> LauncherResult<()> { + let cf = init!(); + + for (provider, entries) in DATA.iter() { + if provider == &Provider::CurseForge && !cf { + continue; + } + + for entry in entries { + if entry.package_type == PackageType::ModPack { + continue; // TODO: Skip modpacks for now + } + + let pkg = provider.get(entry.id).await?; + let ver_id = entry.version_id.to_string(); + let versions = provider.get_versions(&[ver_id.clone()]).await?; + let ver = versions.first().expect("No version found"); + + let db_model = packages::download_package(&pkg, ver, None).await?; + assert!(db_model.hash == ver.files.first().unwrap().sha1); + + let dir = Dirs::get_package_dir( + &db_model.package_type, + &db_model.provider, + &db_model.package_id, + ) + .await?; + let dest = dir.join(packages::join_package_file( + &dir, + &db_model.version_id, + &db_model.file_name, + )); + + assert!(dest.exists(), "File does not exist: {dest:?}"); + } + } + + Ok(()) +} + +// TODO: Add more tests diff --git a/packages/core/src/api/packages/tests.rs b/packages/core/src/api/packages/tests.rs deleted file mode 100644 index 4e07d944..00000000 --- a/packages/core/src/api/packages/tests.rs +++ /dev/null @@ -1,73 +0,0 @@ -use onelauncher_entity::package::Provider; - -use crate::api::packages::provider::ProviderExt; -use crate::api::packages::{self}; -use crate::error::LauncherResult; -use crate::store::{Core, CoreOptions, Dirs}; - -#[tokio::test] -pub async fn test_get_provider() -> LauncherResult<()> { - Core::initialize(CoreOptions::default()).await?; - - let provider = Provider::Modrinth; - - let res = provider.get("oneconfig").await?; - - assert_eq!(res.id, "AibBIVmj"); - - Ok(()) -} - -#[tokio::test] -pub async fn test_get_multiple() -> LauncherResult<()> { - Core::initialize(CoreOptions::default()).await?; - - let provider = Provider::Modrinth; - - let res = provider - .get_multiple(&["oneconfig".to_string(), "chatting".to_string()]) - .await?; - - assert_eq!(res.len(), 2); - - Ok(()) -} - -#[tokio::test] -pub async fn test_get_versions() -> LauncherResult<()> { - Core::initialize(CoreOptions::default()).await?; - - let provider = Provider::Modrinth; - - let res = provider - .get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()]) - .await?; - - assert_eq!(res.len(), 2); - - Ok(()) -} - -#[tokio::test] -pub async fn test_download_chatting() -> LauncherResult<()> { - Core::initialize(CoreOptions::default()).await?; - - let provider = Provider::Modrinth; - - let pkg = provider.get("chatting").await?; - let ver_id = pkg.version_ids.first().expect("No version found"); - let versions = provider.get_versions(&[ver_id.clone()]).await?; - let ver = versions.first().expect("No version found"); - - let db_model = packages::download_package(&pkg, ver, None).await; - assert!(db_model.is_ok_and(|m| m.display_name == pkg.name)); - - let dir = Dirs::get_package_dir(&pkg.package_type, &provider, &pkg.id).await?; - let dest = dir.join(&ver.files.first().unwrap().file_name); - - assert!(dest.exists(), "File does not exist: {dest:?}"); - - Ok(()) -} - -// TODO: Add more tests diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 24e57e9a..47bca235 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -118,14 +118,14 @@ pub trait TauriLauncherApi { async fn get_package_versions( provider: Provider, slug: String, - mc_versions: Option>, - loaders: Option>, + mc_version: Option, + loader: Option, offset: usize, limit: usize, ) -> LauncherResult>; - #[taurpc(alias = "getPackageUser")] - async fn get_package_user(provider: Provider, slug: String) -> LauncherResult; + // #[taurpc(alias = "getPackageUser")] + // async fn get_package_user(provider: Provider, slug: String) -> LauncherResult; #[taurpc(alias = "downloadPackage")] async fn download_package( @@ -485,24 +485,16 @@ impl TauriLauncherApi for TauriLauncherApiImpl { self, provider: Provider, slug: String, - mc_versions: Option>, - loaders: Option>, + mc_version: Option, + loader: Option, offset: usize, limit: usize, ) -> LauncherResult> { provider - .get_versions_paginated(&slug, mc_versions, loaders, offset, limit) + .get_versions_paginated(&slug, mc_version, loader, offset, limit) .await } - async fn get_package_user( - self, - provider: Provider, - slug: String, - ) -> LauncherResult { - provider.get_user(&slug).await - } - async fn download_package( self, provider: Provider, diff --git a/packages/core/src/constants.rs b/packages/core/src/constants.rs index 108835b3..6d00c3d3 100644 --- a/packages/core/src/constants.rs +++ b/packages/core/src/constants.rs @@ -20,7 +20,7 @@ pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL"; /// The Modrinth API base url. pub const MODRINTH_API_URL: &str = "https://api.modrinth.com"; /// The CurseForge API base url. -pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com"; +pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com/v1"; /// The Minecraft game ID on CurseForge. pub const CURSEFORGE_GAME_ID: u32 = 432; /// Our metadata API base url. diff --git a/packages/core/src/store/core_options.rs b/packages/core/src/store/core_options.rs index 9941f807..438195a8 100644 --- a/packages/core/src/store/core_options.rs +++ b/packages/core/src/store/core_options.rs @@ -26,6 +26,7 @@ pub struct CoreOptions { } impl Default for CoreOptions { + #[cfg(not(test))] fn default() -> Self { Self { discord_client_id: None, @@ -40,6 +41,26 @@ impl Default for CoreOptions { logger_filter: None, } } + + #[cfg(test)] + fn default() -> Self { + Self { + discord_client_id: std::env::var("DISCORD_CLIENT_ID").ok(), + launcher_name: String::from("Launcher"), + launcher_version: String::from(env!("CARGO_PKG_VERSION")), + launcher_website: String::from("https://polyfrost.org/"), + fetch_attempts: 3, + msa_client_id: std::env::var("MSA_CLIENT_ID") + .ok() + .unwrap_or_else(|| String::from("00000000402b5328")), + msa_redirect_uri: std::env::var("MSA_REDIRECT_URI") + .ok() + .unwrap_or_else(|| String::from("https://login.live.com/oauth20_desktop.srf")), + curseforge_api_key: std::env::var("CURSEFORGE_API_KEY").ok(), + logger_span_formatting: None, + logger_filter: None, + } + } } impl Core { diff --git a/packages/core/src/store/ingress.rs b/packages/core/src/store/ingress.rs index 84756d3c..e4c7ce14 100644 --- a/packages/core/src/store/ingress.rs +++ b/packages/core/src/store/ingress.rs @@ -113,8 +113,8 @@ pub struct Ingress { pub last_sent: f64, } -// #[onelauncher_macro::specta] -#[derive(Serialize, Deserialize, specta::Type, Debug, Clone)] +#[onelauncher_macro::specta] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum IngressType { Download { file_name: String }, JavaPrepare, diff --git a/packages/entity/Cargo.toml b/packages/entity/Cargo.toml index 8430f58b..4908ba4b 100644 --- a/packages/entity/Cargo.toml +++ b/packages/entity/Cargo.toml @@ -18,7 +18,6 @@ path = "src/lib.rs" default = [] specta = [ "dep:specta", - "onelauncher_macro/specta", "interpulse/specta" ] diff --git a/packages/entity/src/domain/icon.rs b/packages/entity/src/domain/icon.rs index efb30bfe..2282896b 100644 --- a/packages/entity/src/domain/icon.rs +++ b/packages/entity/src/domain/icon.rs @@ -6,10 +6,8 @@ use sea_orm::DeriveValueType; use sea_orm::sea_query::Nullable; use serde::{Deserialize, Serialize}; -// #[onelauncher_macro::specta] -#[derive( - Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type, -)] +#[onelauncher_macro::specta] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)] pub struct Icon(String); impl Nullable for Icon { diff --git a/packages/entity/src/domain/package.rs b/packages/entity/src/domain/package.rs index 44efdda7..8545b6e6 100644 --- a/packages/entity/src/domain/package.rs +++ b/packages/entity/src/domain/package.rs @@ -4,10 +4,13 @@ use sea_orm::{DeriveActiveEnum, EnumIter}; use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)] +#[derive( + Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum, +)] #[serde(rename_all = "lowercase")] #[sea_orm(rs_type = "u8", db_type = "Integer")] pub enum PackageType { + #[default] Mod = 0, ResourcePack = 1, Shader = 2, @@ -29,6 +32,32 @@ impl PackageType { } } +impl From for PackageType { + fn from(value: String) -> Self { + match value.to_lowercase().as_str() { + "mod" => Self::Mod, + "resourcepack" => Self::ResourcePack, + "shader" => Self::Shader, + "datapack" => Self::DataPack, + "modpack" => Self::ModPack, + _ => Self::Mod, // default case + } + } +} + +impl ToString for PackageType { + fn to_string(&self) -> String { + match self { + &Self::Mod => "mod", + &Self::ResourcePack => "resourcepack", + &Self::Shader => "shader", + &Self::DataPack => "datapack", + &Self::ModPack => "modpack", + } + .to_string() + } +} + #[onelauncher_macro::specta] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)] #[sea_orm(rs_type = "u8", db_type = "Integer")] diff --git a/packages/macro/Cargo.toml b/packages/macro/Cargo.toml index fdacd467..719a8161 100644 --- a/packages/macro/Cargo.toml +++ b/packages/macro/Cargo.toml @@ -17,8 +17,6 @@ proc-macro = true [features] default = [] -tauri = [ "specta" ] -specta = [] [dependencies] syn = { workspace = true } diff --git a/packages/macro/src/error.rs b/packages/macro/src/error.rs index 4512a5a9..c1094bf3 100644 --- a/packages/macro/src/error.rs +++ b/packages/macro/src/error.rs @@ -16,9 +16,16 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { for variant in &data_enum.variants { let v_ident = &variant.ident; + let cfg_attrs = &variant + .attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .collect::>(); + match &variant.fields { Fields::Unit => { serialize_arms.push(quote! { + #(#cfg_attrs)* Self::#v_ident => { let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; s.serialize_field("type", stringify!(#v_ident))?; @@ -43,6 +50,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { if has_from && !has_skip { serialize_arms.push(quote! { + #(#cfg_attrs)* Self::#v_ident(inner) => { let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; s.serialize_field("type", stringify!(#v_ident))?; @@ -52,6 +60,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { }); } else { serialize_arms.push(quote! { + #(#cfg_attrs)* Self::#v_ident(_) => { let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; s.serialize_field("type", stringify!(#v_ident))?; @@ -62,6 +71,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { } } else { serialize_arms.push(quote! { + #(#cfg_attrs)* Self::#v_ident(..) => { let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; s.serialize_field("type", stringify!(#v_ident))?; @@ -73,6 +83,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { } Fields::Named(_) => { serialize_arms.push(quote! { + #(#cfg_attrs)* Self::#v_ident { .. } => { let mut s = serializer.serialize_struct(stringify!(#ident), 2)?; s.serialize_field("type", stringify!(#v_ident))?; @@ -99,7 +110,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { }; let output = quote! { - #[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))] + #[derive(onelauncher_macro::SerializedError)] #input #serialize_impl @@ -160,6 +171,7 @@ pub fn error_derive(input: TokenStream) -> TokenStream { let enum_name_str = enum_name.to_string(); let output = quote! { + #[cfg(feature = "specta")] #[derive(Debug, specta::Type, serde::Serialize)] #[specta(remote = #enum_name)] #[specta(tag = "type", content = "data")] diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 20138247..d0df3e14 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -1,10 +1,6 @@ -#[cfg(feature = "specta")] -mod specta; - -#[cfg(feature = "tauri")] mod error; - mod pin; +mod specta; /// This macro is used to derive the `specta::Type` trait for the annotated item. /// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes) @@ -23,7 +19,6 @@ mod pin; /// #[cfg_attr(feature = "specta", specta(rename = #rename))] /// ``` #[proc_macro_attribute] -#[cfg(feature = "specta")] pub fn specta( attr: proc_macro::TokenStream, item: proc_macro::TokenStream,