api modifications

This commit is contained in:
Wyatt Verchere 2024-01-30 17:42:58 -08:00
parent bdde054036
commit 6b99f82cea
15 changed files with 873 additions and 54 deletions

20
Cargo.lock generated
View File

@ -2498,6 +2498,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "minidump-common" name = "minidump-common"
version = "0.14.0" version = "0.14.0"
@ -3582,6 +3592,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"native-tls", "native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
@ -5237,6 +5248,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.13" version = "0.3.13"

View File

@ -46,7 +46,7 @@ indicatif = { version = "0.17.3", optional = true }
async-tungstenite = { version = "0.22.1", features = ["tokio-runtime", "tokio-native-tls"] } async-tungstenite = { version = "0.22.1", features = ["tokio-runtime", "tokio-native-tls"] }
futures = "0.3" futures = "0.3"
reqwest = { version = "0.11", features = ["json", "stream"] } reqwest = { version = "0.11", features = ["json", "stream", "multipart", "blocking"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1", features = ["fs"] } tokio-stream = { version = "0.1", features = ["fs"] }
async-recursion = "1.0.4" async-recursion = "1.0.4"

View File

@ -14,6 +14,9 @@ use crate::{
/// (Does not include modrinth://) /// (Does not include modrinth://)
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> { pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Ok(match sublink.split_once('/') { Ok(match sublink.split_once('/') {
Some(("shared_profile", link)) => {
CommandPayload::OpenSharedProfile { link: link.to_string() }
},
// /mod/{id} - Installs a mod of mod id // /mod/{id} - Installs a mod of mod id
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() }, Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
// /version/{id} - Installs a specific version of id // /version/{id} - Installs a specific version of id

View File

@ -10,6 +10,7 @@ pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
pub mod safety; pub mod safety;
pub mod shared_profile;
pub mod settings; pub mod settings;
pub mod tags; pub mod tags;
@ -37,5 +38,6 @@ pub mod prelude {
jre::JavaVersion, jre::JavaVersion,
}, },
State, State,
shared_profile,
}; };
} }

View File

@ -215,7 +215,7 @@ async fn import_atlauncher_unmanaged(
.clone() .clone()
.unwrap_or_else(|| backup_name.to_string()); .unwrap_or_else(|| backup_name.to_string());
prof.install_stage = ProfileInstallStage::PackInstalling; prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.linked_data = Some(LinkedData { prof.metadata.linked_data = Some(LinkedData::ModrinthModpack {
project_id: description.project_id.clone(), project_id: description.project_id.clone(),
version_id: description.version_id.clone(), version_id: description.version_id.clone(),
locked: Some( locked: Some(

View File

@ -159,7 +159,7 @@ pub fn get_profile_from_pack(
} => CreatePackProfile { } => CreatePackProfile {
name: title, name: title,
icon_url, icon_url,
linked_data: Some(LinkedData { linked_data: Some(LinkedData::ModrinthModpack {
project_id: Some(project_id), project_id: Some(project_id),
version_id: Some(version_id), version_id: Some(version_id),
locked: Some(true), locked: Some(true),
@ -394,13 +394,21 @@ pub async fn set_profile_information(
prof.metadata.linked_data = if project_id.is_some() prof.metadata.linked_data = if project_id.is_some()
&& version_id.is_some() && version_id.is_some()
{ {
Some(LinkedData { Some(LinkedData::ModrinthModpack {
project_id, project_id,
version_id, version_id,
locked: if !ignore_lock { locked: if !ignore_lock {
Some(true) Some(true)
} else { } else {
prof.metadata.linked_data.as_ref().and_then(|x| x.locked) prof.metadata.linked_data.as_ref().and_then(|x| if let LinkedData::ModrinthModpack {
locked: Some(locked),
..
} = x
{
Some(*locked)
} else {
None
})
}, },
}) })
} else { } else {

View File

@ -102,10 +102,10 @@ pub async fn profile_create(
} }
profile.metadata.linked_data = linked_data; profile.metadata.linked_data = linked_data;
if let Some(linked_data) = &mut profile.metadata.linked_data { if let Some(LinkedData::ModrinthModpack{ project_id, version_id, locked, .. }) = &mut profile.metadata.linked_data {
linked_data.locked = Some( *locked = Some(
linked_data.project_id.is_some() project_id.is_some()
&& linked_data.version_id.is_some(), && version_id.is_some(),
); );
} }

View File

@ -3,6 +3,7 @@
use crate::event::emit::{ use crate::event::emit::{
emit_loading, init_loading, loading_try_for_each_concurrent, emit_loading, init_loading, loading_try_for_each_concurrent,
}; };
use crate::state::LinkedData;
use crate::event::LoadingBarType; use crate::event::LoadingBarType;
use crate::pack::install_from::{ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
@ -67,13 +68,11 @@ pub async fn get(
let state = State::get().await?; let state = State::get().await?;
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
let mut profile = profiles.0.get(path).cloned(); let mut profile = profiles.0.get(path).cloned();
if clear_projects.unwrap_or(false) { if clear_projects.unwrap_or(false) {
if let Some(profile) = &mut profile { if let Some(profile) = &mut profile {
profile.projects = HashMap::new(); profile.projects = HashMap::new();
} }
} }
Ok(profile) Ok(profile)
} }
@ -118,14 +117,14 @@ pub async fn get_mod_full_path(
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
if get(profile_path, Some(true)).await?.is_some() { if get(profile_path, Some(true)).await?.is_some() {
let full_path = io::canonicalize( let full_path = io::canonicalize(
project_path.get_full_path(profile_path.clone()).await?, project_path.get_full_path(&profile_path).await?,
)?; )?;
return Ok(full_path); return Ok(full_path);
} }
Err(crate::ErrorKind::OtherError(format!( Err(crate::ErrorKind::OtherError(format!(
"Tried to get the full path of a nonexistent or unloaded project at path {}!", "Tried to get the full path of a nonexistent or unloaded project at path {}!",
project_path.get_full_path(profile_path.clone()).await?.display() project_path.get_full_path(&profile_path).await?.display()
)) ))
.into()) .into())
} }
@ -874,7 +873,13 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
let res = if updated_recent_playtime > 0 { let res = if updated_recent_playtime > 0 {
// Create update struct to send to Labrinth // Create update struct to send to Labrinth
let modrinth_pack_version_id = let modrinth_pack_version_id =
profile.metadata.linked_data.and_then(|l| l.version_id); profile.metadata.linked_data.and_then(|l|
if let LinkedData::ModrinthModpack {
version_id,
..
} = l {
Some(version_id)
} else { None });
let playtime_update_json = json!({ let playtime_update_json = json!({
"seconds": updated_recent_playtime, "seconds": updated_recent_playtime,
"loader": profile.metadata.loader.to_string(), "loader": profile.metadata.loader.to_string(),
@ -892,7 +897,7 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
fetch::post_json( fetch::post_json(
"https://api.modrinth.com/analytics/playtime", "https://staging-api.modrinth.com/analytics/playtime",
serde_json::to_value(hashmap)?, serde_json::to_value(hashmap)?,
&state.fetch_semaphore, &state.fetch_semaphore,
&creds, &creds,

View File

@ -6,7 +6,7 @@ use crate::{
pack::{self, install_from::generate_pack_from_version_id}, pack::{self, install_from::generate_pack_from_version_id},
prelude::{ProfilePathId, ProjectPathId}, prelude::{ProfilePathId, ProjectPathId},
profile::get, profile::get,
state::{ProfileInstallStage, Project}, state::{ProfileInstallStage, Project, LinkedData},
LoadingBarType, State, LoadingBarType, State,
}; };
use futures::try_join; use futures::try_join;
@ -30,15 +30,13 @@ pub async fn update_managed_modrinth_version(
}; };
// Extract modrinth pack information, if appropriate // Extract modrinth pack information, if appropriate
let linked_data = profile let Some(LinkedData::ModrinthModpack{
.metadata project_id: Some(ref project_id),
.linked_data version_id: Some(ref version_id),
.as_ref() ..
.ok_or_else(unmanaged_err)?; }) = profile.metadata.linked_data else {
let project_id: &String = return Err(unmanaged_err().into());
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; };
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// Replace the pack with the new version // Replace the pack with the new version
replace_managed_modrinth( replace_managed_modrinth(
@ -107,15 +105,13 @@ pub async fn repair_managed_modrinth(
.await?; .await?;
// Extract modrinth pack information, if appropriate // Extract modrinth pack information, if appropriate
let linked_data = profile let Some(LinkedData::ModrinthModpack{
.metadata project_id: Some(ref project_id),
.linked_data version_id: Some(ref version_id),
.as_ref() ..
.ok_or_else(unmanaged_err)?; }) = profile.metadata.linked_data else {
let project_id: &String = return Err(unmanaged_err().into());
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?; };
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// Replace the pack with the same version // Replace the pack with the same version
replace_managed_modrinth( replace_managed_modrinth(

View File

@ -0,0 +1,656 @@
use std::path::PathBuf;
use chrono::{DateTime,Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::{config::MODRINTH_API_URL_INTERNAL, prelude::{LinkedData, ModLoader, ProfilePathId, ProjectMetadata, ProjectPathId}, profile, util::{fetch::{fetch_advanced, REQWEST_CLIENT}, io}, state::{Profile, Profiles}};
#[derive(Deserialize, Serialize, Debug)]
pub struct SharedProfile {
pub id: String,
pub name: String,
pub is_owned: bool, // Whether we are the owner (intentionally redundant)
pub owner_id: String,
pub icon_url: Option<String>,
pub loader: ModLoader,
pub loader_version: String,
pub game_version: String,
pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub versions: Vec<String>,
pub overrides: Vec<SharedProfileOverride>,
pub share_links: Option<Vec<SharedProfileLink>>,
pub users: Option<Vec<String>>
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SharedProfileLink {
pub id: String,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SharedProfileOverride {
pub url: String,
pub install_path: PathBuf,
pub hashes: SharedProfileOverrideHashes,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SharedProfileOverrideHashes {
pub sha1: String,
pub sha512: String,
}
// Create a new shared profile from ProfilePathId
// This converts the LinkedData to a SharedProfile and uploads it to the Labrinth API
#[tracing::instrument]
pub async fn create(
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// Currently existing linked data should fail
match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { .. }) => {
return Err(crate::ErrorKind::OtherError("Profile already linked to a shared profile".to_string()).as_error());
},
Some(LinkedData::ModrinthModpack { .. }) => {
return Err(crate::ErrorKind::OtherError("Profile already linked to a modrinth project".to_string()).as_error());
},
None => {}
};
let name = profile.metadata.name;
let loader = profile.metadata.loader;
let loader_version = profile.metadata.loader_version;
let game_version = profile.metadata.game_version;
let modrinth_projects : Vec<_> = profile.projects.iter()
.filter_map(|(_, project)|if let ProjectMetadata::Modrinth { ref version, .. } = project.metadata {
Some(&version.id)
} else {
None
}).collect();
let override_files : Vec<_> = profile.projects.iter()
.filter_map(|(id, project)|if let ProjectMetadata::Inferred { ..} = project.metadata {
Some(id)
} else {
None
}).collect();
// Create the profile on the Labrinth API
let response = REQWEST_CLIENT
.post(
format!("{MODRINTH_API_URL_INTERNAL}client/profile"),
).header("Authorization", &creds.session)
.json(&serde_json::json!({
"name": name,
"loader": loader.as_api_str(),
"loader_version": loader_version.map(|x| x.id).unwrap_or_default(),
"game": "minecraft-java",
"game_version": game_version,
"versions": modrinth_projects,
}))
.send().await?;
let profile_response = response.json::<serde_json::Value>().await?;
// Extract the profile ID from the response
let shared_profile_id = profile_response["id"].as_str().ok_or_else(|| crate::ErrorKind::OtherError("Could not parse response from Labrinth API".to_string()))?.to_string();
// Unmanaged projects
let mut data = vec![]; // 'data' field, giving installation context to labrinth
let mut parts = vec![]; // 'parts' field, containing the actual files
for override_file in override_files {
let path = override_file.get_inner_path_unix();
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else { continue };
// Load override to file
let full_path = &override_file.get_full_path(&profile_id).await?;
let file_bytes = io::read(full_path).await?;
let ext = full_path.extension().and_then(|x| x.to_str()).unwrap_or_default();
let mime = project_file_type(ext).ok_or_else(|| crate::ErrorKind::OtherError(format!("Could not determine file type for {}", ext)))?;
data.push(serde_json::json!({
"file_name": name.clone(),
"install_path": path
}));
let part = reqwest::multipart::Part::bytes(file_bytes).file_name(name.clone()).mime_str(mime)?;
parts.push((name.clone(), part));
}
// Build multipart with 'data' field first
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
let json_part = reqwest::multipart::Part::text(serde_json::to_string(&data)?);//mime_str("application/json")?;
multipart = multipart.part("data", json_part);
for (name, part) in parts {
multipart = multipart.part(name, part);
}
let response = REQWEST_CLIENT.post(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile_id}/override"),
)
.header("Authorization", &creds.session)
.multipart(multipart);
response.send().await?.error_for_status()?;
// Update the profile with the new linked data
profile::edit(&profile_id, |profile| {
let shared_profile_id = shared_profile_id.clone();
profile.metadata.linked_data = Some(LinkedData::SharedProfile {
profile_id: shared_profile_id,
is_owner: true,
});
async { Ok(()) }
}).await?;
// Sync
crate::State::sync().await?;
Ok(())
}
pub fn project_file_type(ext: &str) -> Option<&str> {
match ext {
"jar" => Some("application/java-archive"),
"zip" | "litemod" => Some("application/zip"),
"mrpack" => Some("application/x-modrinth-modpack+zip"),
_ => None,
}
}
#[tracing::instrument]
pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
let state = crate::State::get().await?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// First, get list of shared profiles the user has access to
#[derive(Deserialize, Serialize, Debug)]
pub struct SharedProfileResponse {
pub id: String,
pub name: String,
pub owner_id: String,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub icon_url: Option<String>,
pub loader: ModLoader,
pub game : String,
pub loader_version: String,
pub game_version: String,
// Present only if we are the owner
pub share_links: Option<Vec<SharedProfileLink>>,
pub users: Option<Vec<String>>,
}
let response = REQWEST_CLIENT
.get(
format!("{MODRINTH_API_URL_INTERNAL}client/user"),
)
.header("Authorization", &creds.session)
.send().await?.error_for_status()?;
let profiles = response.json::<Vec<SharedProfileResponse>>().await?;
// Next, get files for each shared profile
// TODO: concurrent requests
#[derive(Serialize, Deserialize)]
pub struct SharedFiles {
pub version_ids: Vec<String>,
pub override_cdns: Vec<SharedProfileOverride>,
}
let mut shared_profiles = vec![];
for profile in profiles.into_iter() {
if profile.game != "minecraft-java" {
continue;
}
let id = profile.id;
let response = REQWEST_CLIENT
.get(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}/files"),
)
.header("Authorization", &creds.session)
.send().await?.error_for_status()?;
let files = response.json::<SharedFiles>().await?;
shared_profiles.push(SharedProfile {
id,
name: profile.name,
is_owned: profile.owner_id == state.credentials.read().await.0.as_ref().map(|x| x.user.id.as_str()).unwrap_or_default(),
owner_id: profile.owner_id,
loader: profile.loader,
loader_version: profile.loader_version,
game_version: profile.game_version,
icon_url: profile.icon_url,
versions: files.version_ids,
overrides: files.override_cdns,
share_links: profile.share_links,
users: profile.users,
updated_at: profile.updated,
created_at: profile.created,
});
}
Ok(shared_profiles)
}
#[tracing::instrument]
pub async fn install(shared_profile : SharedProfile) -> crate::Result<ProfilePathId> {
let state = crate::State::get().await?;
let linked_data = LinkedData::SharedProfile {
profile_id: shared_profile.id,
is_owner: shared_profile.is_owned,
};
// Create new profile
let profile_id = crate::profile::create::profile_create(
shared_profile.name,
shared_profile.game_version,
shared_profile.loader,
Some(shared_profile.loader_version),
None,
shared_profile.icon_url,
Some(linked_data),
None,
None,
).await?;
// Get the profile
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
// TODO: concurrent requests
// Add projects
for version in shared_profile.versions {
profile.add_project_version(version).await?;
}
for file_override in shared_profile.overrides {
let file = fetch_advanced(
Method::GET,
&file_override.url,
Some(file_override.hashes.sha1.as_str()),
None,
None,
None,
&state.fetch_semaphore,
&creds,
)
.await?;
profile.add_project_bytes_directly(&file_override.install_path, file).await?;
}
Ok(profile_id)
}
// Structure repesenting a synchronization difference between a local profile and a shared profile
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct SharedModpackFileUpdate {
// Can be false if all other fields are empty
// if the metadata is different
pub is_synced: bool,
// Projects that are in the local profile but not in the shared profile
pub unsynced_projects: Vec<ProjectPathId>,
// Projects that are in the shared profile but not in the local profile
pub missing_versions: Vec<String>,
pub missing_overrides: Vec<SharedProfileOverride>,
}
#[tracing::instrument]
pub async fn check_updated(profile_id: &ProfilePathId, shared_profile : &SharedProfile) -> crate::Result<SharedModpackFileUpdate> {
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
// Check if the metadata is the same- if different, we return false with no file updates
if profile.metadata.name != shared_profile.name ||
profile.metadata.loader != shared_profile.loader ||
profile.metadata.loader_version.map(|x| x.id).unwrap_or_default() != shared_profile.loader_version ||
profile.metadata.game_version != shared_profile.game_version {
return Ok(SharedModpackFileUpdate::default());
}
// Check if the projects are the same- we check each override by hash and each modrinth project by version id
let mut modrinth_projects = shared_profile.versions.clone();
let mut overrides = shared_profile.overrides.clone();
let unsynced_projects : Vec<_> = profile.projects.into_iter().filter_map(|(id, project)|{
match project.metadata {
ProjectMetadata::Modrinth { ref version, .. } => {
if modrinth_projects.contains(&version.id) {
modrinth_projects.retain(|x| x != &version.id);
}
else {
return Some(id);
}
},
ProjectMetadata::Inferred { .. } => {
let Some(matching_override) = overrides.iter().position(|o| o.install_path.to_string_lossy().to_string() == id.get_inner_path_unix().0)
else {
return Some(id);
};
if let Some(o) = overrides.get(matching_override) {
if o.hashes.sha512 != project.sha512 {
return Some(id);
}
} else {
return Some(id);
}
overrides.remove(matching_override);
}
ProjectMetadata::Unknown => {
// TODO: What to do for unknown projects?
return Some(id)
}
}
None
}).collect();
Ok(SharedModpackFileUpdate {
is_synced: modrinth_projects.is_empty() && overrides.is_empty() && unsynced_projects.is_empty(),
unsynced_projects,
missing_versions: modrinth_projects,
missing_overrides: overrides,
})
}
// Updates projects for a given ProfilePathId from a SharedProfile
// This updates the local profile to match the shared profile on the Labrinth API
#[tracing::instrument]
pub async fn inbound_sync(
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
// Get linked
let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { ref profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()),
};
// Get updated shared profile
let shared_profile = get_all().await?.into_iter().find(|x| &x.id == shared_profile).ok_or_else(|| crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()))?;
let update_data = check_updated(&profile_id, &shared_profile).await?;
if update_data.is_synced {
return Ok(());
}
// Remove projects- unsynced projects need to be removed
for project in update_data.unsynced_projects {
profile.remove_project(&project, None).await?;
}
// TODO: concurrent requests
// Add projects- missing projects need to be added
for version in update_data.missing_versions {
profile.add_project_version(version).await?;
}
for file_override in update_data.missing_overrides {
let file = fetch_advanced(
Method::GET,
&file_override.url,
Some(file_override.hashes.sha1.as_str()),
None,
None,
None,
&state.fetch_semaphore,
&creds,
)
.await?;
profile.add_project_bytes_directly(&file_override.install_path, file).await?;
}
Ok(())
}
// Updates metadata for a given ProfilePathId to the Labrinth API
// Must be an owner of the shared profile
#[tracing::instrument]
pub async fn outbound_sync(
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// Get linked
let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()),
};
// Get updated shared profile
let shared_profile = get_all().await?.into_iter().find(|x| x.id == shared_profile).ok_or_else(|| crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()))?;
// Check owner
if !shared_profile.is_owned {
return Err(crate::ErrorKind::OtherError("Profile is not owned by the current user".to_string()).as_error());
}
// Check if we are synced
let update_data = check_updated(&profile_id, &shared_profile).await?;
let id = shared_profile.id;
if update_data.is_synced {
return Ok(());
}
let unsynced = update_data.unsynced_projects;
let projects : Vec<_> = profile.projects.clone().into_iter().filter(|(id, _)| unsynced.contains(id)).collect();
let unsynced_modrinth_projects : Vec<_> = projects.iter()
.filter_map(|(_, project)|if let ProjectMetadata::Modrinth { ref version, .. } = project.metadata {
Some(&version.id)
} else {
None
}).collect();
let unsynced_override_files : Vec<_> = projects.iter()
.filter_map(|(id, project)|if let ProjectMetadata::Inferred { ..} = project.metadata {
Some(id)
} else {
None
}).collect();
// Generate new version set
let mut new_version_set = shared_profile.versions;
for version in update_data.missing_versions {
new_version_set.retain(|x| x != &version);
}
for version in unsynced_modrinth_projects {
new_version_set.push(version.to_string());
}
// Update metadata + versions
REQWEST_CLIENT
.patch(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}"),
)
.header("Authorization", &creds.session)
.json(&serde_json::json!({
"name": profile.metadata.name,
"loader": profile.metadata.loader.as_api_str(),
"loader_version": profile.metadata.loader_version.map(|x| x.id).unwrap_or_default(),
"game": "minecraft-java",
"game_version": profile.metadata.game_version,
"versions": new_version_set,
}))
.send().await?.error_for_status()?;
// Create multipart for uploading new overrides
let mut parts = vec![]; // 'parts' field, containing the actual files
let mut data = vec![]; // 'data' field, giving installation context to labrinth
for override_file in unsynced_override_files {
let path = override_file.get_inner_path_unix();
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else { continue };
// Load override to file
let full_path = &override_file.get_full_path(&profile_id).await?;
let file_bytes = io::read(full_path).await?;
let ext = full_path.extension().and_then(|x| x.to_str()).unwrap_or_default();
let mime = project_file_type(ext).ok_or_else(|| crate::ErrorKind::OtherError(format!("Could not determine file type for {}", ext)))?;
data.push(serde_json::json!({
"file_name": name.clone(),
"install_path": path
}));
let part = reqwest::multipart::Part::bytes(file_bytes).file_name(name.clone()).mime_str(mime)?;
parts.push((name.clone(), part));
}
// Build multipart with 'data' field first
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
let json_part = reqwest::multipart::Part::text(serde_json::to_string(&data)?);//mime_str("application/json")?;
multipart = multipart.part("data", json_part);
for (name, part) in parts {
multipart = multipart.part(name, part);
}
let response = REQWEST_CLIENT.post(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}/override"),
)
.header("Authorization", &creds.session)
.multipart(multipart);
response.send().await?.error_for_status()?;
// Cannot fail, simply re-checks its synced with the shared profile
Profiles::update_shared_projects().await;
Ok(())
}
pub async fn remove_shared_profile_users(
profile_id: ProfilePathId,
users: Vec<String>,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()),
};
REQWEST_CLIENT
.patch(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"),
)
.header("Authorization", &creds.session)
.json(&serde_json::json!({
"remove_users": users,
}))
.send().await?.error_for_status()?;
Ok(())
}
pub async fn remove_shared_profile_links(
profile_id: ProfilePathId,
links: Vec<String>,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()),
};
REQWEST_CLIENT
.patch(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"),
)
.header("Authorization", &creds.session)
.json(&serde_json::json!({
"remove_links": links,
}))
.send().await?.error_for_status()?;
Ok(())
}
pub async fn generate_share_link(
profile_id: ProfilePathId,
) -> crate::Result<String> {
let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()),
};
let response = REQWEST_CLIENT
.post(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}/share"),
)
.header("Authorization", &creds.session)
.send().await?.error_for_status()?;
let link = response.json::<SharedProfileLink>().await?;
Ok(generate_deep_link(&link))
}
fn generate_deep_link(
link: &SharedProfileLink
) -> String {
format!("modrinth://shared_profile/{}", link.id)
}
pub async fn accept_share_link(
link: String,
) -> crate::Result<()> {
let state = crate::State::get().await?;
let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
REQWEST_CLIENT
.post(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/share/{link}/accept"),
)
.header("Authorization", &creds.session)
.send().await?.error_for_status()?;
Ok(())
}

View File

@ -1,3 +1,4 @@
//! Configuration structs //! Configuration structs
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/"; pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
pub const MODRINTH_API_URL_INTERNAL: &str = "https://staging-api.modrinth.com/_internal/";

View File

@ -229,6 +229,9 @@ pub enum CommandPayload {
// run or install .mrpack // run or install .mrpack
path: PathBuf, path: PathBuf,
}, },
OpenSharedProfile {
link: String,
},
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]

View File

@ -247,13 +247,16 @@ impl State {
tokio::task::spawn(async { tokio::task::spawn(async {
if let Ok(state) = crate::State::get().await { if let Ok(state) = crate::State::get().await {
if !*state.offline.read().await { if !*state.offline.read().await {
// Resolve update_creds first, as it might affect calls made by other updates
let _ = CredentialsStore::update_creds().await;
let res1 = Profiles::update_modrinth_versions(); let res1 = Profiles::update_modrinth_versions();
let res2 = Tags::update(); let res2 = Tags::update();
let res3 = Metadata::update(); let res3 = Metadata::update();
let res4 = Profiles::update_projects(); let res4 = Profiles::update_projects();
let res5 = Settings::update_java(); let res5 = Settings::update_java();
let res6 = CredentialsStore::update_creds(); let res6 = Settings::update_default_user();
let res7 = Settings::update_default_user(); let res7 = Profiles::update_shared_projects();
let _ = join!(res1, res2, res3, res4, res5, res6, res7); let _ = join!(res1, res2, res3, res4, res5, res6, res7);
} }

View File

@ -116,7 +116,7 @@ pub struct ModrinthAuthFlow {
impl ModrinthAuthFlow { impl ModrinthAuthFlow {
pub async fn new(provider: &str) -> crate::Result<Self> { pub async fn new(provider: &str) -> crate::Result<Self> {
let (socket, _) = async_tungstenite::tokio::connect_async(format!( let (socket, _) = async_tungstenite::tokio::connect_async(format!(
"wss://api.modrinth.com/v2/auth/ws?provider={provider}" "wss://staging-api.modrinth.com/v2/auth/ws?provider={provider}"
)) ))
.await?; .await?;
Ok(Self { socket }) Ok(Self { socket })
@ -209,7 +209,7 @@ async fn get_result_from_res(
} }
} }
#[derive(Deserialize)] #[derive(Deserialize, Debug)]
struct Session { struct Session {
session: String, session: String,
} }
@ -351,6 +351,7 @@ pub async fn refresh_credentials(
} }
} }
credentials_store.save().await?;
Ok(()) Ok(())
} }

View File

@ -4,13 +4,14 @@ use crate::data::DirectoryInfo;
use crate::event::emit::{emit_profile, emit_warning}; use crate::event::emit::{emit_profile, emit_warning};
use crate::event::ProfilePayloadType; use crate::event::ProfilePayloadType;
use crate::prelude::JavaVersion; use crate::prelude::JavaVersion;
use crate::shared_profile::SharedModpackFileUpdate;
use crate::state::projects::Project; use crate::state::projects::Project;
use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType}; use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
use crate::util::fetch::{ use crate::util::fetch::{
fetch, fetch_json, write, write_cached_icon, IoSemaphore, fetch, fetch_json, write, write_cached_icon, IoSemaphore,
}; };
use crate::util::io::{self, IOError}; use crate::util::io::{self, IOError};
use crate::State; use crate::{shared_profile, State};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use daedalus::get_hash; use daedalus::get_hash;
use daedalus::modded::LoaderVersion; use daedalus::modded::LoaderVersion;
@ -164,7 +165,7 @@ impl ProjectPathId {
pub async fn get_full_path( pub async fn get_full_path(
&self, &self,
profile: ProfilePathId, profile: &ProfilePathId,
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
let profile_dir = profile.get_full_path().await?; let profile_dir = profile.get_full_path().await?;
Ok(profile_dir.join(&self.0)) Ok(profile_dir.join(&self.0))
@ -209,7 +210,14 @@ pub struct Profile {
pub hooks: Option<Hooks>, pub hooks: Option<Hooks>,
pub projects: HashMap<ProjectPathId, Project>, pub projects: HashMap<ProjectPathId, Project>,
#[serde(default)] #[serde(default)]
pub modrinth_update_version: Option<String>, pub sync_update_version: Option<ProfileUpdateData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum ProfileUpdateData {
ModrinthModpack(String),
SharedProfile(SharedModpackFileUpdate),
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -244,12 +252,19 @@ pub struct ProfileMetadata {
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LinkedData { #[serde(rename_all = "snake_case")]
pub project_id: Option<String>, pub enum LinkedData {
pub version_id: Option<String>, ModrinthModpack {
project_id: Option<String>,
version_id: Option<String>,
#[serde(default = "default_locked")] #[serde(default = "default_locked")]
pub locked: Option<bool>, locked: Option<bool>,
},
SharedProfile {
profile_id: String,
is_owner: bool,
},
} }
// Called if linked_data is present but locked is not // Called if linked_data is present but locked is not
@ -344,7 +359,7 @@ impl Profile {
resolution: None, resolution: None,
fullscreen: None, fullscreen: None,
hooks: None, hooks: None,
modrinth_update_version: None, sync_update_version: None,
}) })
} }
@ -424,6 +439,10 @@ impl Profile {
&creds, &creds,
) )
.await?; .await?;
// Check existing shared profiles for updates
tokio::task::spawn(Profiles::update_shared_projects());
drop(creds); drop(creds);
let mut new_profiles = state.profiles.write().await; let mut new_profiles = state.profiles.write().await;
@ -580,6 +599,52 @@ impl Profile {
Ok((path, version)) Ok((path, version))
} }
#[tracing::instrument(skip(self))]
#[theseus_macros::debug_pin]
pub async fn add_project_bytes_directly(
&self,
relative_path: &Path,
bytes: bytes::Bytes,
) -> crate::Result<ProjectPathId> {
let state = State::get().await?;
let file_path = self
.get_profile_full_path()
.await?
.join(relative_path);
let project_path_id = ProjectPathId::new(&relative_path);
write(&file_path, &bytes, &state.io_semaphore).await?;
let file_name = relative_path
.file_name()
.ok_or_else(|| {
crate::ErrorKind::InputError(
format!("Could not find file name for {:?}", relative_path),
)
})?
.to_string_lossy();
let hash = get_hash(bytes).await?;
{
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
profile.projects.insert(
project_path_id.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Unknown,
file_name: file_name.to_string(),
},
);
profile.metadata.date_modified = Utc::now();
}
}
Ok(project_path_id)
}
#[tracing::instrument(skip(self, bytes))] #[tracing::instrument(skip(self, bytes))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn add_project_bytes( pub async fn add_project_bytes(
@ -872,14 +937,16 @@ impl Profiles {
pub async fn update_modrinth_versions() { pub async fn update_modrinth_versions() {
let res = async { let res = async {
let state = State::get().await?; let state = State::get().await?;
// First, we'll fetch updates for all Modrinth modpacks
// Temporarily store all profiles that have modrinth linked data // Temporarily store all profiles that have modrinth linked data
let mut modrinth_updatables: Vec<(ProfilePathId, String)> = let mut modrinth_updatables: Vec<(ProfilePathId, String)> =
Vec::new(); Vec::new();
{ {
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
for (profile_path, profile) in profiles.0.iter() { for (profile_path, profile) in profiles.0.iter() {
if let Some(linked_data) = &profile.metadata.linked_data { if let Some(LinkedData::ModrinthModpack { project_id, .. }) = &profile.metadata.linked_data {
if let Some(linked_project) = &linked_data.project_id { if let Some(linked_project) = &project_id {
modrinth_updatables.push(( modrinth_updatables.push((
profile_path.clone(), profile_path.clone(),
linked_project.clone(), linked_project.clone(),
@ -922,10 +989,10 @@ impl Profiles {
.contains(&loader.as_api_str().to_string()) .contains(&loader.as_api_str().to_string())
}); });
if let Some(recent_version) = recent_version { if let Some(recent_version) = recent_version {
profile.modrinth_update_version = profile.sync_update_version =
Some(recent_version.id.clone()); Some(ProfileUpdateData::ModrinthModpack(recent_version.id.clone()));
} else { } else {
profile.modrinth_update_version = None; profile.sync_update_version = None;
} }
} }
drop(new_profiles); drop(new_profiles);
@ -953,6 +1020,60 @@ impl Profiles {
}; };
} }
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn update_shared_projects() {
let res = async {
let state = State::get().await?;
// Next, get updates for all shared profiles
// Get SharedProfiles for all available
let shared_profiles = shared_profile::get_all().await?;
let mut update_profiles = HashMap::new();
{
let profiles = state.profiles.read().await;
for (profile_path, profile) in profiles.0.iter() {
if let Some(LinkedData::SharedProfile { profile_id, .. }) = &profile.metadata.linked_data {
if let Some(shared_profile) = shared_profiles.iter().find(|x| x.id == *profile_id) {
// Check for update
let update = shared_profile::check_updated(profile_path, shared_profile).await?;
update_profiles.insert(profile_path.clone(), update);
}
}
}
}
{
let mut new_profiles = state.profiles.write().await;
for (profile_path, update) in update_profiles.iter() {
if let Some(profile) = new_profiles.0.get_mut(&profile_path) {
profile.sync_update_version = Some(ProfileUpdateData::SharedProfile(update.clone()));
}
}
}
{
let profiles = state.profiles.read().await;
profiles.sync().await?;
for (profile_path, _) in update_profiles.iter() {
let Some(profile) = profiles.0.get(&profile_path) else { continue; };
emit_profile(
profile.uuid,
&profile_path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
}
}
Ok::<(), crate::Error>(())
}.await;
match res {
Ok(()) => {}
Err(err) => {
tracing::warn!("Unable to update modrinth versions: {err}")
}
};
}
#[tracing::instrument(skip(self, profile))] #[tracing::instrument(skip(self, profile))]
#[theseus_macros::debug_pin] #[theseus_macros::debug_pin]
pub async fn insert( pub async fn insert(