diff --git a/Cargo.lock b/Cargo.lock index e2f1d1370..f7bda7d7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -777,12 +777,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.9.0" @@ -2210,15 +2204,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - [[package]] name = "overload" version = "0.1.1" @@ -2511,18 +2496,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "pretty_assertions" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" -dependencies = [ - "ctor", - "diff", - "output_vt100", - "yansi", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3553,7 +3526,6 @@ dependencies = [ name = "theseus" version = "0.1.0" dependencies = [ - "argh", "async-tungstenite", "async_zip", "bincode", @@ -3565,8 +3537,6 @@ dependencies = [ "futures", "lazy_static", "log", - "once_cell", - "pretty_assertions", "regex", "reqwest", "serde", @@ -4621,12 +4591,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - [[package]] name = "zeroize" version = "1.6.0" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index 4503a17e7..4cde70b51 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -34,7 +34,6 @@ tracing-error = "0.2" async-tungstenite = { version = "0.20.0", features = ["tokio-runtime", "tokio-native-tls"] } futures = "0.3" -once_cell = "1.9.0" reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["fs"] } @@ -45,8 +44,4 @@ dunce = "1.0.3" [target.'cfg(windows)'.dependencies] winreg = "0.11.0" -[dev-dependencies] -argh = "0.1.6" -pretty_assertions = "1.1.0" - [features] diff --git a/theseus/src/api/auth.rs b/theseus/src/api/auth.rs index 389d62c9b..3ec7b60e1 100644 --- a/theseus/src/api/auth.rs +++ b/theseus/src/api/auth.rs @@ -46,7 +46,7 @@ pub async fn authenticate( )) })?; - let credentials = flow.extract_credentials().await?; + let credentials = flow.extract_credentials(&state.io_semaphore).await?; users.insert(&credentials)?; if state.settings.read().await.default_user.is_none() { @@ -60,13 +60,11 @@ pub async fn authenticate( /// Refresh some credentials using Hydra, if needed /// This is the primary desired way to get credentials, as it will also refresh them. #[tracing::instrument] -pub async fn refresh( - user: uuid::Uuid, - update_name: bool, -) -> crate::Result { +pub async fn refresh(user: uuid::Uuid) -> crate::Result { let state = State::get().await?; let mut users = state.users.write().await; + let io_sempahore = &state.io_semaphore; futures::future::ready(users.get(user)?.ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to refresh nonexistent user with ID {user}" @@ -75,10 +73,7 @@ pub async fn refresh( })) .and_then(|mut credentials| async move { if chrono::offset::Utc::now() > credentials.expires { - inner::refresh_credentials(&mut credentials).await?; - if update_name { - inner::refresh_username(&mut credentials).await?; - } + inner::refresh_credentials(&mut credentials, io_sempahore).await?; } users.insert(&credentials)?; Ok(credentials) diff --git a/theseus/src/api/pack.rs b/theseus/src/api/pack.rs index 93f927024..9fbd534a0 100644 --- a/theseus/src/api/pack.rs +++ b/theseus/src/api/pack.rs @@ -79,6 +79,7 @@ pub async fn install_pack_from_version_id( Method::GET, &format!("{}version/{}", MODRINTH_API_URL, version_id), None, + None, &state.io_semaphore, ) .await?; @@ -104,6 +105,7 @@ pub async fn install_pack_from_version_id( Method::GET, &format!("{}project/{}", MODRINTH_API_URL, version.project_id), None, + None, &state.io_semaphore, ) .await?; @@ -230,7 +232,7 @@ async fn install_pack( let profile = profile.clone(); async move { - // TODO: Future update: prompt user for optional files in a modpack + //TODO: Future update: prompt user for optional files in a modpack if let Some(env) = project.env { if env .get(&EnvType::Client) diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index d7c9c0c19..64d4a605c 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -13,7 +13,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use tokio::{process::Command, sync::RwLock}; +use tokio::{fs, process::Command, sync::RwLock}; /// Remove a profile #[tracing::instrument] @@ -67,23 +67,10 @@ pub async fn list() -> crate::Result #[tracing::instrument] pub async fn sync(path: &Path) -> crate::Result<()> { let state = State::get().await?; + let mut profiles = state.profiles.write().await; - if let Some(profile) = get(path).await? { - let paths = profile.get_profile_project_paths()?; - let projects = crate::state::infer_data_from_files( - paths, - state.directories.caches_dir(), - &state.io_semaphore, - ) - .await?; - - { - let mut profiles = state.profiles.write().await; - if let Some(profile) = profiles.0.get_mut(path) { - profile.projects = projects; - } - } - + if let Some(profile) = profiles.0.get_mut(path) { + profile.sync().await?; State::sync().await?; Ok(()) @@ -95,6 +82,99 @@ pub async fn sync(path: &Path) -> crate::Result<()> { } } +/// Add a project from a version +#[tracing::instrument] +pub async fn add_project_from_version( + profile: &Path, + version_id: String, +) -> crate::Result { + let state = State::get().await?; + let mut profiles = state.profiles.write().await; + + if let Some(profile) = profiles.0.get_mut(profile) { + profile.add_project_version(version_id).await + } else { + Err(crate::ErrorKind::UnmanagedProfileError( + profile.display().to_string(), + ) + .as_error()) + } +} + +/// Add a project from an FS path +#[tracing::instrument] +pub async fn add_project_from_path( + profile: &Path, + path: &Path, + project_type: Option, +) -> crate::Result { + let state = State::get().await?; + let mut profiles = state.profiles.write().await; + + if let Some(profile) = profiles.0.get_mut(profile) { + let file = fs::read(path).await?; + let file_name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + profile + .add_project_bytes( + &file_name, + bytes::Bytes::from(file), + project_type.and_then(|x| serde_json::from_str(&x).ok()), + ) + .await + } else { + Err(crate::ErrorKind::UnmanagedProfileError( + profile.display().to_string(), + ) + .as_error()) + } +} + +/// Toggle whether a project is disabled or not +#[tracing::instrument] +pub async fn toggle_disable_project( + profile: &Path, + project: &Path, +) -> crate::Result<()> { + let state = State::get().await?; + let mut profiles = state.profiles.write().await; + + if let Some(profile) = profiles.0.get_mut(profile) { + profile.toggle_disable_project(project).await?; + + Ok(()) + } else { + Err(crate::ErrorKind::UnmanagedProfileError( + profile.display().to_string(), + ) + .as_error()) + } +} + +/// Remove a project from a profile +#[tracing::instrument] +pub async fn remove_project( + profile: &Path, + project: &Path, +) -> crate::Result<()> { + let state = State::get().await?; + let mut profiles = state.profiles.write().await; + + if let Some(profile) = profiles.0.get_mut(profile) { + profile.remove_project(project).await?; + + Ok(()) + } else { + Err(crate::ErrorKind::UnmanagedProfileError( + profile.display().to_string(), + ) + .as_error()) + } +} /// Run Minecraft using a profile and the default credentials, logged in credentials, /// failing with an error if no credentials are available #[tracing::instrument(skip_all)] @@ -104,13 +184,13 @@ pub async fn run(path: &Path) -> crate::Result>> { // Get default account and refresh credentials (preferred way to log in) let default_account = state.settings.read().await.default_user; let credentials = if let Some(default_account) = default_account { - refresh(default_account, false).await? + refresh(default_account).await? } else { // If no default account, try to use a logged in account let users = auth::users().await?; let last_account = users.iter().next(); if let Some(last_account) = last_account { - refresh(last_account.id, false).await? + refresh(last_account.id).await? } else { return Err(crate::ErrorKind::NoCredentialsError.as_error()); } @@ -123,7 +203,7 @@ pub async fn run(path: &Path) -> crate::Result>> { #[tracing::instrument(skip_all)] pub async fn run_credentials( path: &Path, - credentials: &crate::auth::Credentials, + credentials: &auth::Credentials, ) -> crate::Result>> { let state = State::get().await?; let settings = state.settings.read().await; diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index 459ca08ec..3c5663406 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -159,10 +159,10 @@ pub async fn profile_create( let settings = state.settings.read().await; let optimal_version_key = jre::get_optimal_jre_key(&profile).await?; if settings.java_globals.get(&optimal_version_key).is_some() { - profile.set_java_settings(Some(JavaSettings { + profile.java = Some(JavaSettings { jre_key: Some(optimal_version_key), extra_arguments: None, - }))?; + }); } else { println!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default."); } diff --git a/theseus/src/config.rs b/theseus/src/config.rs index 18a121a24..b21c6017e 100644 --- a/theseus/src/config.rs +++ b/theseus/src/config.rs @@ -1,29 +1,13 @@ //! Configuration structs -use once_cell::sync::Lazy; -use std::time; +use lazy_static::lazy_static; -pub static BINCODE_CONFIG: Lazy = - Lazy::new(|| { +lazy_static! { + pub static ref BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard() .with_little_endian() - .with_no_limit() - }); - -pub static REQWEST_CLIENT: Lazy = Lazy::new(|| { - let mut headers = reqwest::header::HeaderMap::new(); - let header = reqwest::header::HeaderValue::from_str(&format!( - "modrinth/daedalus/{} (support@modrinth.com)", - env!("CARGO_PKG_VERSION") - )) - .unwrap(); - headers.insert(reqwest::header::USER_AGENT, header); - reqwest::Client::builder() - .tcp_keepalive(Some(time::Duration::from_secs(10))) - .default_headers(headers) - .build() - .expect("Reqwest Client Building Failed") -}); + .with_no_limit(); +} pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/"; diff --git a/theseus/src/launcher/auth.rs b/theseus/src/launcher/auth.rs index ae7c557d1..e6f9c523e 100644 --- a/theseus/src/launcher/auth.rs +++ b/theseus/src/launcher/auth.rs @@ -1,10 +1,13 @@ //! Authentication flow based on Hydra +use crate::util::fetch::{fetch_advanced, fetch_json}; use async_tungstenite as ws; use bincode::{Decode, Encode}; use chrono::{prelude::*, Duration}; use futures::prelude::*; use lazy_static::lazy_static; +use reqwest::Method; use serde::{Deserialize, Serialize}; +use tokio::sync::{RwLock, Semaphore}; use url::Url; lazy_static! { @@ -93,7 +96,10 @@ impl HydraAuthFlow { )) } - pub async fn extract_credentials(&mut self) -> crate::Result { + pub async fn extract_credentials( + &mut self, + semaphore: &RwLock, + ) -> crate::Result { // Minecraft bearer token let token_resp = self .socket @@ -111,7 +117,7 @@ impl HydraAuthFlow { Utc::now() + Duration::seconds(token.expires_after.into()); // Get account credentials - let info = fetch_info(&token.token).await?; + let info = fetch_info(&token.token, semaphore).await?; // Return structure from response Ok(Credentials { @@ -127,17 +133,16 @@ impl HydraAuthFlow { pub async fn refresh_credentials( credentials: &mut Credentials, + semaphore: &RwLock, ) -> crate::Result<()> { - let resp = crate::config::REQWEST_CLIENT - .post(HYDRA_URL.join("/refresh")?) - .json( - &serde_json::json!({ "refresh_token": credentials.refresh_token }), - ) - .send() - .await? - .error_for_status()? - .json::() - .await?; + let resp = fetch_json::( + Method::POST, + HYDRA_URL.join("/refresh")?.as_str(), + None, + Some(serde_json::json!({ "refresh_token": credentials.refresh_token })), + semaphore, + ) + .await?; credentials.access_token = resp.token; credentials.refresh_token = resp.refresh_token; @@ -147,24 +152,21 @@ pub async fn refresh_credentials( Ok(()) } -pub async fn refresh_username( - credentials: &mut Credentials, -) -> crate::Result<()> { - let info = fetch_info(&credentials.access_token).await?; - credentials.username = info.name; - Ok(()) -} - // Helpers -async fn fetch_info(token: &str) -> crate::Result { - let url = - Url::parse("https://api.minecraftservices.com/minecraft/profile")?; - Ok(crate::config::REQWEST_CLIENT - .get(url) - .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}")) - .send() - .await? - .error_for_status()? - .json::() - .await?) +async fn fetch_info( + token: &str, + semaphore: &RwLock, +) -> crate::Result { + let result = fetch_advanced( + Method::GET, + "https://api.minecraftservices.com/minecraft/profile", + None, + None, + Some(("Authorization", &format!("Bearer {token}"))), + semaphore, + ) + .await?; + let value = serde_json::from_slice(&result)?; + + Ok(value) } diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 4e7d3bded..941c83311 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -100,13 +100,14 @@ impl State { // On launcher initialization, attempt a tag fetch after tags init let mut tags = Tags::init(&database)?; - if let Err(tag_fetch_err) = tags.fetch_update().await { + if let Err(tag_fetch_err) = + tags.fetch_update(&io_semaphore).await + { tracing::error!( "Failed to fetch tags on launcher init: {}", tag_fetch_err ); }; - // On launcher initialization, if global java variables are unset, try to find and set them // (they are required for the game to launch) if settings.java_globals.count() == 0 { diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index 50633ace6..f75d7be3e 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -1,11 +1,16 @@ use super::settings::{Hooks, MemorySettings, WindowSize}; +use crate::config::MODRINTH_API_URL; use crate::data::DirectoryInfo; use crate::state::projects::Project; -use crate::util::fetch::write_cached_icon; +use crate::state::{ModrinthVersion, ProjectType}; +use crate::util::fetch::{fetch, fetch_json, write, write_cached_icon}; +use crate::State; use daedalus::modded::LoaderVersion; use dunce::canonicalize; use futures::prelude::*; +use reqwest::Method; use serde::{Deserialize, Serialize}; +use std::io::Cursor; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -129,12 +134,19 @@ impl Profile { Ok(self) } - #[tracing::instrument] - pub fn set_java_settings( - &mut self, - java: Option, - ) -> crate::Result<()> { - self.java = java; + pub async fn sync(&mut self) -> crate::Result<()> { + let state = State::get().await?; + + let paths = self.get_profile_project_paths()?; + let projects = crate::state::infer_data_from_files( + paths, + state.directories.caches_dir(), + &state.io_semaphore, + ) + .await?; + + self.projects = projects; + Ok(()) } @@ -153,13 +165,150 @@ impl Profile { Ok::<(), crate::Error>(()) }; - read_paths("mods")?; - read_paths("shaders")?; - read_paths("resourcepacks")?; - read_paths("datapacks")?; + read_paths(ProjectType::Mod.get_folder())?; + read_paths(ProjectType::ShaderPack.get_folder())?; + read_paths(ProjectType::ResourcePack.get_folder())?; + read_paths(ProjectType::DataPack.get_folder())?; Ok(files) } + + pub async fn add_project_version( + &mut self, + version_id: String, + ) -> crate::Result { + let state = State::get().await?; + + let version = fetch_json::( + Method::GET, + &format!("{MODRINTH_API_URL}version/{version_id}"), + None, + None, + &state.io_semaphore, + ) + .await?; + + let file = if let Some(file) = version.files.iter().find(|x| x.primary) + { + file + } else if let Some(file) = version.files.first() { + file + } else { + return Err(crate::ErrorKind::InputError( + "No files for input version present!".to_string(), + ) + .into()); + }; + + let bytes = fetch( + &file.url, + file.hashes.get("sha1").map(|x| &**x), + &state.io_semaphore, + ) + .await?; + + let path = self + .add_project_bytes( + &file.filename, + bytes, + ProjectType::get_from_loaders(version.loaders), + ) + .await?; + + Ok(path) + } + + pub async fn add_project_bytes( + &mut self, + file_name: &str, + bytes: bytes::Bytes, + project_type: Option, + ) -> crate::Result { + let project_type = if let Some(project_type) = project_type { + project_type + } else { + let cursor = Cursor::new(&*bytes); + + let mut archive = zip::ZipArchive::new(cursor).map_err(|_| { + crate::ErrorKind::InputError( + "Unable to infer project type for input file".to_string(), + ) + })?; + if archive.by_name("fabric.mod.json").is_ok() + || archive.by_name("quilt.mod.json").is_ok() + || archive.by_name("META-INF/mods.toml").is_ok() + || archive.by_name("mcmod.info").is_ok() + { + ProjectType::Mod + } else if archive.by_name("pack.mcmeta").is_ok() { + if archive.file_names().any(|x| x.starts_with("data/")) { + ProjectType::DataPack + } else { + ProjectType::ResourcePack + } + } else { + return Err(crate::ErrorKind::InputError( + "Unable to infer project type for input file".to_string(), + ) + .into()); + } + }; + + let state = State::get().await?; + let path = self.path.join(project_type.get_folder()).join(file_name); + write(&path, &bytes, &state.io_semaphore).await?; + + self.sync().await?; + + Ok(path) + } + + pub async fn toggle_disable_project( + &mut self, + path: &Path, + ) -> crate::Result<()> { + if let Some(mut project) = self.projects.remove(path) { + let path = path.to_path_buf(); + let mut new_path = path.clone(); + + if path.extension().map_or(false, |ext| ext == "disabled") { + project.disabled = false; + } else { + new_path.set_file_name(format!( + "{}.disabled", + path.file_name().unwrap_or_default().to_string_lossy() + )); + project.disabled = true; + } + + fs::rename(path, &new_path).await?; + + self.projects.insert(new_path, project); + } else { + return Err(crate::ErrorKind::InputError(format!( + "Project path does not exist: {:?}", + path + )) + .into()); + } + + Ok(()) + } + + pub async fn remove_project(&mut self, path: &Path) -> crate::Result<()> { + if self.projects.contains_key(path) { + fs::remove_file(path).await?; + self.projects.remove(path); + } else { + return Err(crate::ErrorKind::InputError(format!( + "Project path does not exist: {:?}", + path + )) + .into()); + } + + Ok(()) + } } impl Profiles { @@ -258,7 +407,7 @@ impl Profiles { stream::iter(self.0.iter()) .map(Ok::<_, crate::Error>) .try_for_each_concurrent(None, |(path, profile)| async move { - let json = serde_json::to_vec_pretty(&profile)?; + let json = serde_json::to_vec(&profile)?; let json_path = Path::new(&path.to_string_lossy().to_string()) .join(PROFILE_JSON_PATH); diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index 6ad8e90de..a9d31d6e3 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -1,9 +1,10 @@ //! Project management + inference -use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT}; -use crate::util::fetch::write_cached_icon; +use crate::config::MODRINTH_API_URL; +use crate::util::fetch::{fetch_json, write_cached_icon}; use async_zip::tokio::read::fs::ZipFileReader; use chrono::{DateTime, Utc}; +use reqwest::Method; use serde::{Deserialize, Serialize}; use serde_json::json; use sha2::Digest; @@ -12,13 +13,52 @@ use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; use tokio::sync::{RwLock, Semaphore}; +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ProjectType { + Mod, + DataPack, + ResourcePack, + ShaderPack, +} + +impl ProjectType { + pub fn get_from_loaders(loaders: Vec) -> Option { + if loaders + .iter() + .any(|x| ["fabric", "forge", "quilt"].contains(&&**x)) + { + Some(ProjectType::Mod) + } else if loaders.iter().any(|x| x == "datapack") { + Some(ProjectType::DataPack) + } else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) { + Some(ProjectType::ShaderPack) + } else if loaders + .iter() + .any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x)) + { + Some(ProjectType::ResourcePack) + } else { + None + } + } + + pub fn get_folder(&self) -> &'static str { + match self { + ProjectType::Mod => "mods", + ProjectType::DataPack => "datapacks", + ProjectType::ResourcePack => "resourcepacks", + ProjectType::ShaderPack => "shaderpacks", + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Project { pub sha512: String, pub disabled: bool, pub metadata: ProjectMetadata, pub file_name: String, - pub update_available: bool, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -223,19 +263,20 @@ pub async fn infer_data_from_files( file_path_hashes.insert(hash, path.clone()); } - let files: HashMap = REQWEST_CLIENT - .post(format!("{}version_files", MODRINTH_API_URL)) - .json(&json!({ + let files: HashMap = fetch_json( + Method::POST, + &format!("{}version_files", MODRINTH_API_URL), + None, + Some(json!({ "hashes": file_path_hashes.keys().collect::>(), "algorithm": "sha512", - })) - .send() - .await? - .json() - .await?; - - let projects: Vec = REQWEST_CLIENT - .get(format!( + })), + io_semaphore, + ) + .await?; + let projects: Vec = fetch_json( + Method::GET, + &format!( "{}projects?ids={}", MODRINTH_API_URL, serde_json::to_string( @@ -244,27 +285,32 @@ pub async fn infer_data_from_files( .map(|x| x.project_id.clone()) .collect::>() )? - )) - .send() - .await? - .json() - .await?; + ), + None, + None, + io_semaphore, + ) + .await?; - let teams: Vec = REQWEST_CLIENT - .get(format!( + let teams: Vec = fetch_json::< + Vec>, + >( + Method::GET, + &format!( "{}teams?ids={}", MODRINTH_API_URL, serde_json::to_string( &projects.iter().map(|x| x.team.clone()).collect::>() )? - )) - .send() - .await? - .json::>>() - .await? - .into_iter() - .flatten() - .collect(); + ), + None, + None, + io_semaphore, + ) + .await? + .into_iter() + .flatten() + .collect(); let mut return_projects = HashMap::new(); let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new(); @@ -297,7 +343,6 @@ pub async fn infer_data_from_files( members: team_members, }, file_name, - update_available: false, }, ); continue; @@ -326,7 +371,6 @@ pub async fn infer_data_from_files( disabled: path.ends_with(".disabled"), metadata: ProjectMetadata::Unknown, file_name, - update_available: false, }, ); continue; @@ -380,7 +424,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Inferred { title: Some( pack.display_name @@ -447,7 +490,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Inferred { title: Some(if pack.name.is_empty() { pack.modid @@ -513,7 +555,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Inferred { title: Some(pack.name.unwrap_or(pack.id)), description: pack.description, @@ -579,7 +620,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Inferred { title: Some( pack.metadata @@ -615,7 +655,7 @@ pub async fn infer_data_from_files( .file() .entries() .iter() - .position(|f| f.entry().filename() == "pack.mcdata"); + .position(|f| f.entry().filename() == "pack.mcmeta"); if let Some(index) = zip_index_option { let file = zip_file_reader.file().entries().get(index).unwrap(); #[derive(Deserialize)] @@ -645,7 +685,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Inferred { title: None, description: pack.description, @@ -666,7 +705,6 @@ pub async fn infer_data_from_files( sha512: hash, disabled: path.ends_with(".disabled"), file_name, - update_available: false, metadata: ProjectMetadata::Unknown, }, ); diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index c5fa1c94e..34e2236eb 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -63,7 +63,7 @@ impl Settings { #[tracing::instrument(skip(self))] pub async fn sync(&self, to: &Path) -> crate::Result<()> { - fs::write(to, serde_json::to_vec_pretty(self)?) + fs::write(to, serde_json::to_vec(self)?) .await .map_err(|err| { crate::ErrorKind::FSError(format!( diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs index a088729b7..be643f10b 100644 --- a/theseus/src/state/tags.rs +++ b/theseus/src/state/tags.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; use bincode::{Decode, Encode}; +use reqwest::Method; use serde::{Deserialize, Serialize}; +use tokio::sync::{RwLock, Semaphore}; -use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL, REQWEST_CLIENT}; +use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL}; +use crate::util::fetch::fetch_json; const CATEGORIES_DB_TREE: &[u8] = b"categories"; const LOADERS_DB_TREE: &[u8] = b"loaders"; @@ -133,13 +136,17 @@ impl Tags { // Fetches the tags from the Modrinth API and stores them in the database #[tracing::instrument(skip(self))] - pub async fn fetch_update(&mut self) -> crate::Result<()> { - let categories = self.fetch_tag("category"); - let loaders = self.fetch_tag("loader"); - let game_versions = self.fetch_tag("game_version"); - let licenses = self.fetch_tag("license"); - let donation_platforms = self.fetch_tag("donation_platform"); - let report_types = self.fetch_tag("report_type"); + pub async fn fetch_update( + &mut self, + semaphore: &RwLock, + ) -> crate::Result<()> { + let categories = format!("{MODRINTH_API_URL}tag/category"); + let loaders = format!("{MODRINTH_API_URL}tag/loader"); + let game_versions = format!("{MODRINTH_API_URL}tag/game_version"); + let licenses = format!("{MODRINTH_API_URL}tag/license"); + let donation_platforms = + format!("{MODRINTH_API_URL}tag/donation_platform"); + let report_types = format!("{MODRINTH_API_URL}tag/report_type"); let ( categories, loaders, @@ -148,70 +155,78 @@ impl Tags { donation_platforms, report_types, ) = tokio::try_join!( - categories, - loaders, - game_versions, - licenses, - donation_platforms, - report_types + fetch_json::>( + Method::GET, + &categories, + None, + None, + semaphore + ), + fetch_json::>( + Method::GET, + &loaders, + None, + None, + semaphore + ), + fetch_json::>( + Method::GET, + &game_versions, + None, + None, + semaphore + ), + fetch_json::>( + Method::GET, + &licenses, + None, + None, + semaphore + ), + fetch_json::>( + Method::GET, + &donation_platforms, + None, + None, + semaphore + ), + fetch_json::>( + Method::GET, + &report_types, + None, + None, + semaphore + ), )?; // Store the tags in the database self.0.categories.insert( "categories", - bincode::encode_to_vec( - categories.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(categories, *BINCODE_CONFIG)?, )?; self.0.loaders.insert( "loaders", - bincode::encode_to_vec( - loaders.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(loaders, *BINCODE_CONFIG)?, )?; self.0.game_versions.insert( "game_versions", - bincode::encode_to_vec( - game_versions.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(game_versions, *BINCODE_CONFIG)?, )?; self.0.licenses.insert( "licenses", - bincode::encode_to_vec( - licenses.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(licenses, *BINCODE_CONFIG)?, )?; self.0.donation_platforms.insert( "donation_platforms", - bincode::encode_to_vec( - donation_platforms.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(donation_platforms, *BINCODE_CONFIG)?, )?; self.0.report_types.insert( "report_types", - bincode::encode_to_vec( - report_types.json::>().await?, - *BINCODE_CONFIG, - )?, + bincode::encode_to_vec(report_types, *BINCODE_CONFIG)?, )?; Ok(()) } - - #[tracing::instrument(skip(self))] - pub async fn fetch_tag( - &self, - tag_type: &str, - ) -> Result { - let url = &format!("{MODRINTH_API_URL}tag/{}", tag_type); - let content = REQWEST_CLIENT.get(url).send().await?; - Ok(content) - } } // Serializeable struct for all tags to be fetched together by the frontend diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index c5384e285..4bff0aeed 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -1,53 +1,86 @@ //! Functions for fetching infromation from the Internet -use crate::config::REQWEST_CLIENT; use bytes::Bytes; +use lazy_static::lazy_static; use reqwest::Method; use serde::de::DeserializeOwned; use std::ffi::OsStr; use std::path::{Path, PathBuf}; +use std::time; use tokio::sync::{RwLock, Semaphore}; use tokio::{ fs::{self, File}, io::AsyncWriteExt, }; +lazy_static! { + static ref REQWEST_CLIENT: reqwest::Client = { + let mut headers = reqwest::header::HeaderMap::new(); + let header = reqwest::header::HeaderValue::from_str(&format!( + "modrinth/theseus/{} (support@modrinth.com)", + env!("CARGO_PKG_VERSION") + )) + .unwrap(); + headers.insert(reqwest::header::USER_AGENT, header); + reqwest::Client::builder() + .tcp_keepalive(Some(time::Duration::from_secs(10))) + .default_headers(headers) + .build() + .expect("Reqwest Client Building Failed") + }; +} const FETCH_ATTEMPTS: usize = 3; +#[tracing::instrument(skip(semaphore))] pub async fn fetch( url: &str, sha1: Option<&str>, semaphore: &RwLock, ) -> crate::Result { - fetch_advanced(Method::GET, url, sha1, semaphore).await + fetch_advanced(Method::GET, url, sha1, None, None, semaphore).await } +#[tracing::instrument(skip(json_body, semaphore))] pub async fn fetch_json( method: Method, url: &str, sha1: Option<&str>, + json_body: Option, semaphore: &RwLock, ) -> crate::Result where T: DeserializeOwned, { - let result = fetch_advanced(method, url, sha1, semaphore).await?; + let result = + fetch_advanced(method, url, sha1, json_body, None, semaphore).await?; let value = serde_json::from_slice(&result)?; Ok(value) } /// Downloads a file with retry and checksum functionality -#[tracing::instrument(skip(semaphore))] +#[tracing::instrument(skip(json_body, semaphore))] pub async fn fetch_advanced( method: Method, url: &str, sha1: Option<&str>, + json_body: Option, + header: Option<(&str, &str)>, semaphore: &RwLock, ) -> crate::Result { let io_semaphore = semaphore.read().await; let _permit = io_semaphore.acquire().await?; - for attempt in 1..=(FETCH_ATTEMPTS + 1) { - let result = REQWEST_CLIENT.request(method.clone(), url).send().await; + for attempt in 1..=(FETCH_ATTEMPTS + 1) { + let mut req = REQWEST_CLIENT.request(method.clone(), url); + + if let Some(body) = json_body.clone() { + req = req.json(&body); + } + + if let Some(header) = header { + req = req.header(header.0, header.1); + } + + let result = req.send().await; match result { Ok(x) => { let bytes = x.bytes().await; diff --git a/theseus_cli/src/subcommands/profile.rs b/theseus_cli/src/subcommands/profile.rs index a46084bd6..10125ecce 100644 --- a/theseus_cli/src/subcommands/profile.rs +++ b/theseus_cli/src/subcommands/profile.rs @@ -330,7 +330,7 @@ impl ProfileRun { )) }) .await?; - let credentials = auth::refresh(id, false).await?; + let credentials = auth::refresh(id).await?; let proc_lock = profile::run_credentials(&path, &credentials).await?; let mut proc = proc_lock.write().await; diff --git a/theseus_gui/src-tauri/src/api/auth.rs b/theseus_gui/src-tauri/src/api/auth.rs index d4d80d641..9e7857563 100644 --- a/theseus_gui/src-tauri/src/api/auth.rs +++ b/theseus_gui/src-tauri/src/api/auth.rs @@ -19,11 +19,8 @@ pub async fn auth_authenticate_await_completion() -> Result { /// Refresh some credentials using Hydra, if needed // invoke('auth_refresh',user) #[tauri::command] -pub async fn auth_refresh( - user: uuid::Uuid, - update_name: bool, -) -> Result { - Ok(auth::refresh(user, update_name).await?) +pub async fn auth_refresh(user: uuid::Uuid) -> Result { + Ok(auth::refresh(user).await?) } /// Remove a user account from the database diff --git a/theseus_gui/src-tauri/src/api/profile.rs b/theseus_gui/src-tauri/src/api/profile.rs index a311884bb..4db06b70f 100644 --- a/theseus_gui/src-tauri/src/api/profile.rs +++ b/theseus_gui/src-tauri/src/api/profile.rs @@ -27,6 +27,51 @@ pub async fn profile_list( Ok(res) } +// Adds a project to a profile from a version ID +// invoke('profile_add_project_from_version') +#[tauri::command] +pub async fn profile_add_project_from_version( + path: &Path, + version_id: String, +) -> Result { + let res = profile::add_project_from_version(path, version_id).await?; + Ok(res) +} + +// Adds a project to a profile from a path +// invoke('profile_add_project_from_path') +#[tauri::command] +pub async fn profile_add_project_from_path( + path: &Path, + project_path: &Path, + project_type: Option, +) -> Result { + let res = profile::add_project_from_path(path, project_path, project_type) + .await?; + Ok(res) +} + +// Toggles disabling a project from its path +// invoke('profile_toggle_disable_project') +#[tauri::command] +pub async fn profile_toggle_disable_project( + path: &Path, + project_path: &Path, +) -> Result<()> { + profile::toggle_disable_project(path, project_path).await?; + Ok(()) +} + +// Removes a project from a profile +// invoke('profile_remove_project') +#[tauri::command] +pub async fn profile_remove_project( + path: &Path, + project_path: &Path, +) -> Result<()> { + profile::remove_project(path, project_path).await?; + Ok(()) +} // Run minecraft using a profile using the default credentials // Returns a u32 representing the PID, which can be used to poll // for the actual Child in the state. diff --git a/theseus_gui/src-tauri/src/api/settings.rs b/theseus_gui/src-tauri/src/api/settings.rs index 1765df2b9..e9f712a40 100644 --- a/theseus_gui/src-tauri/src/api/settings.rs +++ b/theseus_gui/src-tauri/src/api/settings.rs @@ -1,5 +1,5 @@ use crate::api::Result; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use theseus::prelude::*; // Identical to theseus::settings::Settings except for the custom_java_args field @@ -43,7 +43,11 @@ pub async fn settings_set(settings: FrontendSettings) -> Result<()> { let backend_settings = Settings { memory: settings.memory, game_resolution: settings.game_resolution, - custom_java_args: settings.custom_java_args.split_whitespace().map(|s| s.to_string()).collect(), + custom_java_args: settings + .custom_java_args + .split_whitespace() + .map(|s| s.to_string()) + .collect(), custom_env_args: settings.custom_env_args, java_globals: settings.java_globals, default_user: settings.default_user, diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index a97634a82..4b03439be 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -23,6 +23,10 @@ fn main() { api::profile::profile_remove, api::profile::profile_get, api::profile::profile_list, + api::profile::profile_add_project_from_version, + api::profile::profile_add_project_from_path, + api::profile::profile_toggle_disable_project, + api::profile::profile_remove_project, api::profile::profile_run, api::profile::profile_run_wait, api::profile::profile_run_credentials, diff --git a/theseus_gui/src/App.vue b/theseus_gui/src/App.vue index 4689b66ba..9b3ca0084 100644 --- a/theseus_gui/src/App.vue +++ b/theseus_gui/src/App.vue @@ -4,9 +4,9 @@ import { RouterView, RouterLink } from 'vue-router' import { ChevronLeftIcon, ChevronRightIcon, + HomeIcon, SearchIcon, - BookIcon, - ClientIcon, + LibraryIcon, PlusIcon, SettingsIcon, } from 'omorphia' @@ -42,9 +42,9 @@ list().then(
- + - + diff --git a/theseus_gui/src/helpers/profile.js b/theseus_gui/src/helpers/profile.js index 4be728e5f..dcd9f312f 100644 --- a/theseus_gui/src/helpers/profile.js +++ b/theseus_gui/src/helpers/profile.js @@ -10,10 +10,10 @@ export async function addDefaultInstance() { return await invoke('profile_create_empty') } -/// Add empty default instance +/// Creates instance /// Returns a path to the profile created -export async function create() { - return await invoke('profile_create') +export async function create(name, game_version, modloader, loader_version, icon) { + return await invoke('profile_create', { name, game_version, modloader, loader_version, icon }) } // Remove a profile @@ -33,6 +33,28 @@ export async function list() { return await invoke('profile_list') } +// Add a project to a profile from a version +// Returns a path to the new project file +export async function add_project_from_version(path, version_id) { + return await invoke('profile_add_project_from_version', { path, version_id }) +} + +// Add a project to a profile from a path + project_type +// Returns a path to the new project file +export async function add_project_from_path(path, project_path, project_type) { + return await invoke('profile_add_project_from_path', { path, project_path, project_type }) +} + +// Toggle disabling a project +export async function toggle_disable_project(path, project_path) { + return await invoke('profile_toggle_disable_project', { path, project_path }) +} + +// Remove a project +export async function remove_project(path, project_path) { + return await invoke('profile_remove_project', { path, project_path }) +} + // Run Minecraft using a pathed profile // Returns PID of child export async function run(path) { diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 76d4b304b..a576acc34 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -5,6 +5,7 @@ use dunce::canonicalize; use theseus::prelude::*; +use theseus::profile_create::profile_create; use tokio::time::{sleep, Duration}; // A simple Rust implementation of the authentication run @@ -46,14 +47,46 @@ async fn main() -> theseus::Result<()> { println!("Creating/adding profile."); - let profile_path = - pack::install_pack_from_version_id("KxUUUFh5".to_string()) - .await - .unwrap(); + let name = "Example".to_string(); + let game_version = "1.19.2".to_string(); + let modloader = ModLoader::Fabric; + let loader_version = "stable".to_string(); + + let profile_path = profile_create( + name.clone(), + game_version, + modloader, + Some(loader_version), + None, + None, + ) + .await?; + + println!("Adding sodium"); + let sodium_path = profile::add_project_from_version( + &profile_path, + "rAfhHfow".to_string(), + ) + .await?; + + let mod_menu_path = profile::add_project_from_version( + &profile_path, + "gSoPJyVn".to_string(), + ) + .await?; + + println!("Disabling sodium"); + profile::toggle_disable_project(&profile_path, &sodium_path).await?; + + profile::remove_project(&profile_path, &mod_menu_path).await?; + // let profile_path = + // pack::install_pack_from_version_id("KxUUUFh5".to_string()) + // .await + // .unwrap(); // async closure for testing any desired edits // (ie: changing the java runtime of an added profile) - // println!("Editing."); + println!("Editing."); profile::edit(&profile_path, |_profile| { // Eg: Java- this would let you change the java runtime of the profile instead of using the default // use theseus::prelude::jre::JAVA__KEY; @@ -72,6 +105,7 @@ async fn main() -> theseus::Result<()> { authenticate_run().await?; // could take credentials from here direct, but also deposited in state users } + println!("running"); // Run a profile, running minecraft and store the RwLock to the process let proc_lock = profile::run(&canonicalize(&profile_path)?).await?;