feat: implement basic modrinth version fetching

This commit is contained in:
uku 2024-07-31 00:01:38 +02:00
parent de92a0f05a
commit 4249195d0f
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
5 changed files with 146 additions and 1 deletions

25
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -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?;

View file

@ -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
View 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()
}
}