api modifications
This commit is contained in:
parent
bdde054036
commit
6b99f82cea
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -2498,6 +2498,16 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "minidump-common"
|
||||
version = "0.14.0"
|
||||
@ -3582,6 +3592,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@ -5237,6 +5248,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
|
||||
@ -46,7 +46,7 @@ indicatif = { version = "0.17.3", optional = true }
|
||||
|
||||
async-tungstenite = { version = "0.22.1", features = ["tokio-runtime", "tokio-native-tls"] }
|
||||
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-stream = { version = "0.1", features = ["fs"] }
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
@ -14,6 +14,9 @@ use crate::{
|
||||
/// (Does not include modrinth://)
|
||||
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
|
||||
Ok(match sublink.split_once('/') {
|
||||
Some(("shared_profile", link)) => {
|
||||
CommandPayload::OpenSharedProfile { link: link.to_string() }
|
||||
},
|
||||
// /mod/{id} - Installs a mod of mod id
|
||||
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
|
||||
// /version/{id} - Installs a specific version of id
|
||||
|
||||
@ -10,6 +10,7 @@ pub mod pack;
|
||||
pub mod process;
|
||||
pub mod profile;
|
||||
pub mod safety;
|
||||
pub mod shared_profile;
|
||||
pub mod settings;
|
||||
pub mod tags;
|
||||
|
||||
@ -37,5 +38,6 @@ pub mod prelude {
|
||||
jre::JavaVersion,
|
||||
},
|
||||
State,
|
||||
shared_profile,
|
||||
};
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ async fn import_atlauncher_unmanaged(
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.linked_data = Some(LinkedData {
|
||||
prof.metadata.linked_data = Some(LinkedData::ModrinthModpack {
|
||||
project_id: description.project_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: Some(
|
||||
|
||||
@ -159,7 +159,7 @@ pub fn get_profile_from_pack(
|
||||
} => CreatePackProfile {
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
linked_data: Some(LinkedData::ModrinthModpack {
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
locked: Some(true),
|
||||
@ -394,13 +394,21 @@ pub async fn set_profile_information(
|
||||
prof.metadata.linked_data = if project_id.is_some()
|
||||
&& version_id.is_some()
|
||||
{
|
||||
Some(LinkedData {
|
||||
Some(LinkedData::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
locked: if !ignore_lock {
|
||||
Some(true)
|
||||
} 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 {
|
||||
|
||||
@ -102,10 +102,10 @@ pub async fn profile_create(
|
||||
}
|
||||
|
||||
profile.metadata.linked_data = linked_data;
|
||||
if let Some(linked_data) = &mut profile.metadata.linked_data {
|
||||
linked_data.locked = Some(
|
||||
linked_data.project_id.is_some()
|
||||
&& linked_data.version_id.is_some(),
|
||||
if let Some(LinkedData::ModrinthModpack{ project_id, version_id, locked, .. }) = &mut profile.metadata.linked_data {
|
||||
*locked = Some(
|
||||
project_id.is_some()
|
||||
&& version_id.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
use crate::state::LinkedData;
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
@ -67,13 +68,11 @@ pub async fn get(
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut profile = profiles.0.get(path).cloned();
|
||||
|
||||
if clear_projects.unwrap_or(false) {
|
||||
if let Some(profile) = &mut profile {
|
||||
profile.projects = HashMap::new();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
@ -118,14 +117,14 @@ pub async fn get_mod_full_path(
|
||||
) -> crate::Result<PathBuf> {
|
||||
if get(profile_path, Some(true)).await?.is_some() {
|
||||
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);
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
"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())
|
||||
}
|
||||
@ -874,7 +873,13 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
|
||||
let res = if updated_recent_playtime > 0 {
|
||||
// Create update struct to send to Labrinth
|
||||
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!({
|
||||
"seconds": updated_recent_playtime,
|
||||
"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;
|
||||
fetch::post_json(
|
||||
"https://api.modrinth.com/analytics/playtime",
|
||||
"https://staging-api.modrinth.com/analytics/playtime",
|
||||
serde_json::to_value(hashmap)?,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
|
||||
@ -6,7 +6,7 @@ use crate::{
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
prelude::{ProfilePathId, ProjectPathId},
|
||||
profile::get,
|
||||
state::{ProfileInstallStage, Project},
|
||||
state::{ProfileInstallStage, Project, LinkedData},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
use futures::try_join;
|
||||
@ -30,15 +30,13 @@ pub async fn update_managed_modrinth_version(
|
||||
};
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
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)?;
|
||||
let Some(LinkedData::ModrinthModpack{
|
||||
project_id: Some(ref project_id),
|
||||
version_id: Some(ref version_id),
|
||||
..
|
||||
}) = profile.metadata.linked_data else {
|
||||
return Err(unmanaged_err().into());
|
||||
};
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
@ -107,15 +105,13 @@ pub async fn repair_managed_modrinth(
|
||||
.await?;
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
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)?;
|
||||
let Some(LinkedData::ModrinthModpack{
|
||||
project_id: Some(ref project_id),
|
||||
version_id: Some(ref version_id),
|
||||
..
|
||||
}) = profile.metadata.linked_data else {
|
||||
return Err(unmanaged_err().into());
|
||||
};
|
||||
|
||||
// Replace the pack with the same version
|
||||
replace_managed_modrinth(
|
||||
|
||||
656
theseus/src/api/shared_profile.rs
Normal file
656
theseus/src/api/shared_profile.rs
Normal 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(())
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
//! 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/";
|
||||
|
||||
@ -229,6 +229,9 @@ pub enum CommandPayload {
|
||||
// run or install .mrpack
|
||||
path: PathBuf,
|
||||
},
|
||||
OpenSharedProfile {
|
||||
link: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
@ -247,13 +247,16 @@ impl State {
|
||||
tokio::task::spawn(async {
|
||||
if let Ok(state) = crate::State::get().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 res2 = Tags::update();
|
||||
let res3 = Metadata::update();
|
||||
let res4 = Profiles::update_projects();
|
||||
let res5 = Settings::update_java();
|
||||
let res6 = CredentialsStore::update_creds();
|
||||
let res7 = Settings::update_default_user();
|
||||
let res6 = Settings::update_default_user();
|
||||
let res7 = Profiles::update_shared_projects();
|
||||
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6, res7);
|
||||
}
|
||||
|
||||
@ -116,7 +116,7 @@ pub struct ModrinthAuthFlow {
|
||||
impl ModrinthAuthFlow {
|
||||
pub async fn new(provider: &str) -> crate::Result<Self> {
|
||||
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?;
|
||||
Ok(Self { socket })
|
||||
@ -209,7 +209,7 @@ async fn get_result_from_res(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Session {
|
||||
session: String,
|
||||
}
|
||||
@ -351,6 +351,7 @@ pub async fn refresh_credentials(
|
||||
}
|
||||
}
|
||||
|
||||
credentials_store.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -4,13 +4,14 @@ use crate::data::DirectoryInfo;
|
||||
use crate::event::emit::{emit_profile, emit_warning};
|
||||
use crate::event::ProfilePayloadType;
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::shared_profile::SharedModpackFileUpdate;
|
||||
use crate::state::projects::Project;
|
||||
use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_json, write, write_cached_icon, IoSemaphore,
|
||||
};
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::State;
|
||||
use crate::{shared_profile, State};
|
||||
use chrono::{DateTime, Utc};
|
||||
use daedalus::get_hash;
|
||||
use daedalus::modded::LoaderVersion;
|
||||
@ -164,7 +165,7 @@ impl ProjectPathId {
|
||||
|
||||
pub async fn get_full_path(
|
||||
&self,
|
||||
profile: ProfilePathId,
|
||||
profile: &ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let profile_dir = profile.get_full_path().await?;
|
||||
Ok(profile_dir.join(&self.0))
|
||||
@ -209,7 +210,14 @@ pub struct Profile {
|
||||
pub hooks: Option<Hooks>,
|
||||
pub projects: HashMap<ProjectPathId, Project>,
|
||||
#[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)]
|
||||
@ -244,12 +252,19 @@ pub struct ProfileMetadata {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LinkedData {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LinkedData {
|
||||
ModrinthModpack {
|
||||
project_id: Option<String>,
|
||||
version_id: Option<String>,
|
||||
#[serde(default = "default_locked")]
|
||||
locked: Option<bool>,
|
||||
},
|
||||
|
||||
#[serde(default = "default_locked")]
|
||||
pub locked: Option<bool>,
|
||||
SharedProfile {
|
||||
profile_id: String,
|
||||
is_owner: bool,
|
||||
},
|
||||
}
|
||||
|
||||
// Called if linked_data is present but locked is not
|
||||
@ -344,7 +359,7 @@ impl Profile {
|
||||
resolution: None,
|
||||
fullscreen: None,
|
||||
hooks: None,
|
||||
modrinth_update_version: None,
|
||||
sync_update_version: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -424,6 +439,10 @@ impl Profile {
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check existing shared profiles for updates
|
||||
tokio::task::spawn(Profiles::update_shared_projects());
|
||||
|
||||
drop(creds);
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
@ -580,6 +599,52 @@ impl Profile {
|
||||
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))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn add_project_bytes(
|
||||
@ -872,14 +937,16 @@ impl Profiles {
|
||||
pub async fn update_modrinth_versions() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
|
||||
// First, we'll fetch updates for all Modrinth modpacks
|
||||
// Temporarily store all profiles that have modrinth linked data
|
||||
let mut modrinth_updatables: Vec<(ProfilePathId, String)> =
|
||||
Vec::new();
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
for (profile_path, profile) in profiles.0.iter() {
|
||||
if let Some(linked_data) = &profile.metadata.linked_data {
|
||||
if let Some(linked_project) = &linked_data.project_id {
|
||||
if let Some(LinkedData::ModrinthModpack { project_id, .. }) = &profile.metadata.linked_data {
|
||||
if let Some(linked_project) = &project_id {
|
||||
modrinth_updatables.push((
|
||||
profile_path.clone(),
|
||||
linked_project.clone(),
|
||||
@ -922,10 +989,10 @@ impl Profiles {
|
||||
.contains(&loader.as_api_str().to_string())
|
||||
});
|
||||
if let Some(recent_version) = recent_version {
|
||||
profile.modrinth_update_version =
|
||||
Some(recent_version.id.clone());
|
||||
profile.sync_update_version =
|
||||
Some(ProfileUpdateData::ModrinthModpack(recent_version.id.clone()));
|
||||
} else {
|
||||
profile.modrinth_update_version = None;
|
||||
profile.sync_update_version = None;
|
||||
}
|
||||
}
|
||||
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))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn insert(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user