feat: implement basic modrinth version fetching
This commit is contained in:
parent
de92a0f05a
commit
4249195d0f
5 changed files with 146 additions and 1 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -218,6 +218,12 @@ dependencies = [
|
|||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
|
@ -385,6 +391,12 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
|
@ -604,6 +616,15 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
|
@ -1561,11 +1582,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||
name = "uklient"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"hex",
|
||||
"itertools",
|
||||
"oauth2",
|
||||
"reqwest 0.12.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
|
|
@ -4,7 +4,10 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
color-eyre = "0.6.3"
|
||||
hex = "0.4.3"
|
||||
itertools = "0.13.0"
|
||||
oauth2 = "4.4.2"
|
||||
reqwest = { version = "0.12.5", default-features = false, features = [
|
||||
"http2",
|
||||
|
@ -14,6 +17,7 @@ reqwest = { version = "0.12.5", default-features = false, features = [
|
|||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
|
||||
|
|
|
@ -55,6 +55,12 @@ pub struct MinecraftProfile {
|
|||
|
||||
// TODO better error handling !!!!
|
||||
pub async fn login() -> Result<MinecraftProfile> {
|
||||
// temporary until i can access api.minecraftservices.com
|
||||
return Ok(MinecraftProfile {
|
||||
id: String::new(),
|
||||
name: String::new(),
|
||||
});
|
||||
|
||||
let access_token = request_msa_token().await?;
|
||||
let (xbox_token, userhash) = auth_xbox_live(&access_token).await?;
|
||||
let (xsts_token, xsts_userhash) = fetch_xsts_token(&xbox_token).await?;
|
||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -1,10 +1,12 @@
|
|||
#![warn(clippy::pedantic)]
|
||||
|
||||
mod auth;
|
||||
mod modrinth;
|
||||
|
||||
use std::{fs::File, sync::LazyLock};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use modrinth::VersionsExt;
|
||||
use reqwest::header::{HeaderMap, USER_AGENT};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{
|
||||
|
@ -38,9 +40,25 @@ async fn main() -> Result<()> {
|
|||
tracing::debug!("starting debug");
|
||||
|
||||
let profile = auth::login().await?;
|
||||
|
||||
tracing::info!("logged in as {} ({})", profile.name, profile.id);
|
||||
|
||||
let versions = modrinth::get_versions().await?;
|
||||
tracing::info!(
|
||||
"available minecraft versions: {:?}",
|
||||
versions.available_minecraft_versions()
|
||||
);
|
||||
|
||||
if let Some(version) = versions.latest_for_mc_version("1.20.4") {
|
||||
tracing::info!(
|
||||
"latest 1.20.4 version: {} ({})",
|
||||
version.name,
|
||||
version.version_number
|
||||
);
|
||||
|
||||
let path = version.download_file().await?;
|
||||
tracing::info!("downloaded file at {:?}", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
92
src/modrinth.rs
Normal file
92
src/modrinth.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use std::{env, fs::File, path::PathBuf};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use color_eyre::eyre::{bail, Report, Result};
|
||||
use itertools::Itertools;
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha512};
|
||||
|
||||
const PROJECT_ID: &str = "JR0bkFKa";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectVersion {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
game_versions: Vec<String>,
|
||||
files: Vec<VersionFile>,
|
||||
date_published: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VersionFile {
|
||||
url: String,
|
||||
filename: String,
|
||||
size: usize,
|
||||
hashes: VersionFileHashes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VersionFileHashes {
|
||||
sha512: String,
|
||||
}
|
||||
|
||||
impl ProjectVersion {
|
||||
pub async fn download_file(&self) -> Result<PathBuf> {
|
||||
let version_file = &self.files[0];
|
||||
let path = env::temp_dir().join(&version_file.filename);
|
||||
|
||||
let response = crate::HTTP_CLIENT.get(&version_file.url).send().await?;
|
||||
let mut bytes = &*response.bytes().await?; // deref to get &[u8]
|
||||
|
||||
if bytes.len() != version_file.size {
|
||||
bail!("invalid length!")
|
||||
}
|
||||
|
||||
let expected_hash = hex::decode(&version_file.hashes.sha512)?;
|
||||
let computed_hash = {
|
||||
let mut sha512 = Sha512::new();
|
||||
sha512.update(bytes);
|
||||
sha512.finalize()
|
||||
};
|
||||
|
||||
if expected_hash[..] != computed_hash[..] {
|
||||
bail!("hash mismatch!")
|
||||
}
|
||||
|
||||
let mut file = File::create(&path)?;
|
||||
std::io::copy(&mut bytes, &mut file)?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_versions() -> Result<Vec<ProjectVersion>> {
|
||||
let url = format!("https://api.modrinth.com/v2/project/{PROJECT_ID}/version");
|
||||
|
||||
crate::HTTP_CLIENT
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Report::from)
|
||||
}
|
||||
|
||||
pub trait VersionsExt {
|
||||
fn available_minecraft_versions(&self) -> Vec<&String>;
|
||||
fn latest_for_mc_version(&self, mc_version: &str) -> Option<&ProjectVersion>;
|
||||
}
|
||||
|
||||
impl VersionsExt for Vec<ProjectVersion> {
|
||||
fn available_minecraft_versions(&self) -> Vec<&String> {
|
||||
// we can safely assume that there is only one game_version per project version since it's a modpack
|
||||
self.iter().map(|v| &v.game_versions[0]).unique().collect()
|
||||
}
|
||||
|
||||
fn latest_for_mc_version(&self, mc_version: &str) -> Option<&ProjectVersion> {
|
||||
self.iter()
|
||||
.filter(|v| v.game_versions.contains(&mc_version.to_owned()))
|
||||
.sorted_by_key(|v| v.date_published)
|
||||
.next_back()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue