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",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.34"
|
version = "0.8.34"
|
||||||
|
@ -385,6 +391,12 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
@ -604,6 +616,15 @@ version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
@ -1561,11 +1582,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
name = "uklient"
|
name = "uklient"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
|
"hex",
|
||||||
|
"itertools",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
"reqwest 0.12.5",
|
"reqwest 0.12.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
|
@ -4,7 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
|
hex = "0.4.3"
|
||||||
|
itertools = "0.13.0"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
reqwest = { version = "0.12.5", default-features = false, features = [
|
reqwest = { version = "0.12.5", default-features = false, features = [
|
||||||
"http2",
|
"http2",
|
||||||
|
@ -14,6 +17,7 @@ reqwest = { version = "0.12.5", default-features = false, features = [
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
sha2 = "0.10.8"
|
||||||
tokio = { version = "1.38.0", features = ["full"] }
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
|
||||||
|
|
|
@ -55,6 +55,12 @@ pub struct MinecraftProfile {
|
||||||
|
|
||||||
// TODO better error handling !!!!
|
// TODO better error handling !!!!
|
||||||
pub async fn login() -> Result<MinecraftProfile> {
|
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 access_token = request_msa_token().await?;
|
||||||
let (xbox_token, userhash) = auth_xbox_live(&access_token).await?;
|
let (xbox_token, userhash) = auth_xbox_live(&access_token).await?;
|
||||||
let (xsts_token, xsts_userhash) = fetch_xsts_token(&xbox_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)]
|
#![warn(clippy::pedantic)]
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod modrinth;
|
||||||
|
|
||||||
use std::{fs::File, sync::LazyLock};
|
use std::{fs::File, sync::LazyLock};
|
||||||
|
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
|
use modrinth::VersionsExt;
|
||||||
use reqwest::header::{HeaderMap, USER_AGENT};
|
use reqwest::header::{HeaderMap, USER_AGENT};
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
|
@ -38,9 +40,25 @@ async fn main() -> Result<()> {
|
||||||
tracing::debug!("starting debug");
|
tracing::debug!("starting debug");
|
||||||
|
|
||||||
let profile = auth::login().await?;
|
let profile = auth::login().await?;
|
||||||
|
|
||||||
tracing::info!("logged in as {} ({})", profile.name, profile.id);
|
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(())
|
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