You've already forked AstralRinth
forked from didirus/AstralRinth
Merge branch 'beta' into release
This commit is contained in:
@@ -28,6 +28,16 @@ pub async fn offline_auth(
|
||||
crate::state::offline_auth(name, &state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn elyby_auth(
|
||||
uuid: uuid::Uuid,
|
||||
login: &str,
|
||||
access_token: &str
|
||||
) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
crate::state::elyby_auth(uuid, login, access_token, &state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::state::ModrinthCredentials;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn authenticate_begin_flow() -> String {
|
||||
pub fn authenticate_begin_flow() -> &'static str {
|
||||
crate::state::get_login_url()
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,12 @@ async fn import_mmc_unmanaged(
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("net.neoforged") {
|
||||
return Some((
|
||||
PackDependency::NeoForge,
|
||||
component.version.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
||||
return Some((
|
||||
PackDependency::QuiltLoader,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//! Configuration structs
|
||||
|
||||
// pub const MODRINTH_URL: &str = "https://staging.modrinth.com/";
|
||||
// pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
|
||||
// pub const MODRINTH_API_URL_V3: &str = "https://staging-api.modrinth.com/v3/";
|
||||
|
||||
pub const MODRINTH_URL: &str = "https://modrinth.com/";
|
||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
||||
pub const MODRINTH_API_URL_V3: &str = "https://api.modrinth.com/v3/";
|
||||
|
||||
pub const MODRINTH_SOCKET_URL: &str = "wss://api.modrinth.com/";
|
||||
|
||||
pub const META_URL: &str = "https://launcher-meta.modrinth.com/";
|
||||
@@ -151,6 +151,41 @@ pub enum ErrorKind {
|
||||
"A skin texture must have a dimension of either 64x64 or 64x32 pixels"
|
||||
)]
|
||||
InvalidSkinTexture,
|
||||
|
||||
#[error(
|
||||
"[AR] Target minecraft {minecraft_version} version doesn't exist."
|
||||
)]
|
||||
InvalidMinecraftVersion {
|
||||
minecraft_version: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"[AR] Target metadata not found for minecraft version {minecraft_version}."
|
||||
)]
|
||||
MinecraftMetadataNotFound {
|
||||
minecraft_version: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"[AR] Network error: {error}"
|
||||
)]
|
||||
NetworkErrorOccurred {
|
||||
error: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"[AR] IO error: {error}"
|
||||
)]
|
||||
IOErrorOccurred {
|
||||
error: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"[AR] Parse error: {reason}"
|
||||
)]
|
||||
ParseError {
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::launcher::download::download_log_config;
|
||||
use crate::launcher::io::IOError;
|
||||
use crate::profile::QuickPlayType;
|
||||
use crate::state::{
|
||||
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||
AccountType, Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||
};
|
||||
use crate::util::io;
|
||||
use crate::util::{io, utils};
|
||||
use crate::{State, get_resource_file, process, state as st};
|
||||
use chrono::Utc;
|
||||
use daedalus as d;
|
||||
@@ -634,16 +634,31 @@ pub async fn launch_minecraft(
|
||||
}
|
||||
|
||||
// [AR] Patch
|
||||
if credentials.access_token == "null" && credentials.refresh_token == "null" {
|
||||
if credentials.account_type == AccountType::Pirate.as_lowercase_str() {
|
||||
if version_jar == "1.16.4" || version_jar == "1.16.5" {
|
||||
let invalid_url = "https://invalid.invalid";
|
||||
tracing::info!("✅ JVM args is patched by AstralRinth for MC {}", version_jar);
|
||||
tracing::info!(
|
||||
"[AR] • The launcher detected the launch of {} on the offline account. Applying offline multiplayer fixes.",
|
||||
version_jar
|
||||
);
|
||||
command.arg("-Dminecraft.api.env=custom");
|
||||
command.arg(format!("-Dminecraft.api.auth.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
|
||||
command
|
||||
.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
|
||||
command
|
||||
.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
|
||||
command
|
||||
.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
|
||||
}
|
||||
} else if credentials.account_type == AccountType::ElyBy.as_lowercase_str()
|
||||
{
|
||||
tracing::info!(
|
||||
"[AR] • The launcher detected the launch of {} on the Ely.by account. Applying Ely.by Java Injector.",
|
||||
version_jar
|
||||
);
|
||||
let path_buf = utils::get_or_download_elyby_injector().await?;
|
||||
let path = path_buf.to_str().unwrap();
|
||||
command.arg(format!("-javaagent:{}=ely.by", path));
|
||||
}
|
||||
|
||||
command
|
||||
@@ -745,10 +760,10 @@ pub async fn launch_minecraft(
|
||||
|
||||
// [AR] Feature
|
||||
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
|
||||
let _ = state
|
||||
.discord_rpc
|
||||
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
|
||||
.await;
|
||||
let _ = state
|
||||
.discord_rpc
|
||||
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
|
||||
.await;
|
||||
|
||||
let _ = state
|
||||
.friends_socket
|
||||
|
||||
@@ -11,7 +11,6 @@ and launching Modrinth mod packs
|
||||
pub mod util; // [AR] Refactor
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
mod error;
|
||||
mod event;
|
||||
mod launcher;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::{META_URL, MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::state::ProjectType;
|
||||
use crate::util::fetch::{FetchSemaphore, fetch_json, sha1_async};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -8,6 +7,7 @@ use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -945,7 +945,7 @@ impl CachedEntry {
|
||||
CacheValueType::Project => {
|
||||
fetch_original_values!(
|
||||
Project,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"projects",
|
||||
CacheValue::Project
|
||||
)
|
||||
@@ -953,7 +953,7 @@ impl CachedEntry {
|
||||
CacheValueType::Version => {
|
||||
fetch_original_values!(
|
||||
Version,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"versions",
|
||||
CacheValue::Version
|
||||
)
|
||||
@@ -961,7 +961,7 @@ impl CachedEntry {
|
||||
CacheValueType::User => {
|
||||
fetch_original_values!(
|
||||
User,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"users",
|
||||
CacheValue::User
|
||||
)
|
||||
@@ -969,7 +969,7 @@ impl CachedEntry {
|
||||
CacheValueType::Team => {
|
||||
let mut teams = fetch_many_batched::<Vec<TeamMember>>(
|
||||
Method::GET,
|
||||
MODRINTH_API_URL_V3,
|
||||
env!("MODRINTH_API_URL_V3"),
|
||||
"teams?ids=",
|
||||
&keys,
|
||||
fetch_semaphore,
|
||||
@@ -1008,7 +1008,7 @@ impl CachedEntry {
|
||||
CacheValueType::Organization => {
|
||||
let mut orgs = fetch_many_batched::<Organization>(
|
||||
Method::GET,
|
||||
MODRINTH_API_URL_V3,
|
||||
env!("MODRINTH_API_URL_V3"),
|
||||
"organizations?ids=",
|
||||
&keys,
|
||||
fetch_semaphore,
|
||||
@@ -1063,7 +1063,7 @@ impl CachedEntry {
|
||||
CacheValueType::File => {
|
||||
let mut versions = fetch_json::<HashMap<String, Version>>(
|
||||
Method::POST,
|
||||
&format!("{MODRINTH_API_URL}version_files"),
|
||||
concat!(env!("MODRINTH_API_URL"), "version_files"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
@@ -1119,7 +1119,11 @@ impl CachedEntry {
|
||||
.map(|x| {
|
||||
(
|
||||
x.key().to_string(),
|
||||
format!("{META_URL}{}/v0/manifest.json", x.key()),
|
||||
format!(
|
||||
"{}{}/v0/manifest.json",
|
||||
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||
x.key()
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1154,7 +1158,7 @@ impl CachedEntry {
|
||||
CacheValueType::MinecraftManifest => {
|
||||
fetch_original_value!(
|
||||
MinecraftManifest,
|
||||
META_URL,
|
||||
env!("MODRINTH_LAUNCHER_META_URL"),
|
||||
format!(
|
||||
"minecraft/v{}/manifest.json",
|
||||
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
||||
@@ -1165,7 +1169,7 @@ impl CachedEntry {
|
||||
CacheValueType::Categories => {
|
||||
fetch_original_value!(
|
||||
Categories,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"tag/category",
|
||||
CacheValue::Categories
|
||||
)
|
||||
@@ -1173,7 +1177,7 @@ impl CachedEntry {
|
||||
CacheValueType::ReportTypes => {
|
||||
fetch_original_value!(
|
||||
ReportTypes,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"tag/report_type",
|
||||
CacheValue::ReportTypes
|
||||
)
|
||||
@@ -1181,7 +1185,7 @@ impl CachedEntry {
|
||||
CacheValueType::Loaders => {
|
||||
fetch_original_value!(
|
||||
Loaders,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"tag/loader",
|
||||
CacheValue::Loaders
|
||||
)
|
||||
@@ -1189,7 +1193,7 @@ impl CachedEntry {
|
||||
CacheValueType::GameVersions => {
|
||||
fetch_original_value!(
|
||||
GameVersions,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"tag/game_version",
|
||||
CacheValue::GameVersions
|
||||
)
|
||||
@@ -1197,7 +1201,7 @@ impl CachedEntry {
|
||||
CacheValueType::DonationPlatforms => {
|
||||
fetch_original_value!(
|
||||
DonationPlatforms,
|
||||
MODRINTH_API_URL,
|
||||
env!("MODRINTH_API_URL"),
|
||||
"tag/donation_platform",
|
||||
CacheValue::DonationPlatforms
|
||||
)
|
||||
@@ -1297,14 +1301,12 @@ impl CachedEntry {
|
||||
}
|
||||
});
|
||||
|
||||
let version_update_url =
|
||||
format!("{MODRINTH_API_URL}version_files/update");
|
||||
let variations =
|
||||
futures::future::try_join_all(filtered_keys.iter().map(
|
||||
|((loaders_key, game_version), hashes)| {
|
||||
fetch_json::<HashMap<String, Version>>(
|
||||
Method::POST,
|
||||
&version_update_url,
|
||||
concat!(env!("MODRINTH_API_URL"), "version_files/update"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
@@ -1368,7 +1370,11 @@ impl CachedEntry {
|
||||
.map(|x| {
|
||||
(
|
||||
x.key().to_string(),
|
||||
format!("{MODRINTH_API_URL}search{}", x.key()),
|
||||
format!(
|
||||
"{}search{}",
|
||||
env!("MODRINTH_API_URL"),
|
||||
x.key()
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::{MODRINTH_API_URL_V3, MODRINTH_SOCKET_URL};
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::event::emit::emit_friend;
|
||||
@@ -77,7 +76,8 @@ impl FriendsSocket {
|
||||
|
||||
if let Some(credentials) = credentials {
|
||||
let mut request = format!(
|
||||
"{MODRINTH_SOCKET_URL}_internal/launcher_socket?code={}",
|
||||
"{}_internal/launcher_socket?code={}",
|
||||
env!("MODRINTH_SOCKET_URL"),
|
||||
credentials.session
|
||||
)
|
||||
.into_client_request()?;
|
||||
@@ -303,7 +303,7 @@ impl FriendsSocket {
|
||||
) -> crate::Result<Vec<UserFriend>> {
|
||||
fetch_json(
|
||||
Method::GET,
|
||||
&format!("{MODRINTH_API_URL_V3}friends"),
|
||||
concat!(env!("MODRINTH_API_URL_V3"), "friends"),
|
||||
None,
|
||||
None,
|
||||
semaphore,
|
||||
@@ -328,7 +328,7 @@ impl FriendsSocket {
|
||||
) -> crate::Result<()> {
|
||||
fetch_advanced(
|
||||
Method::POST,
|
||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
||||
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -349,7 +349,7 @@ impl FriendsSocket {
|
||||
) -> crate::Result<()> {
|
||||
fetch_advanced(
|
||||
Method::DELETE,
|
||||
&format!("{MODRINTH_API_URL_V3}friend/{user_id}"),
|
||||
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -131,6 +131,7 @@ where
|
||||
expires: legacy_credentials.expires,
|
||||
active: minecraft_auth.default_user == Some(uuid)
|
||||
|| minecraft_users_len == 1,
|
||||
account_type: legacy_credentials.account_type,
|
||||
}
|
||||
.upsert(exec)
|
||||
.await?;
|
||||
@@ -518,6 +519,7 @@ struct LegacyCredentials {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub account_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
||||
@@ -85,21 +85,18 @@ pub struct MinecraftLoginFlow {
|
||||
pub verifier: String,
|
||||
pub challenge: String,
|
||||
pub session_id: String,
|
||||
pub redirect_uri: String,
|
||||
pub auth_request_uri: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn login_begin(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<MinecraftLoginFlow> {
|
||||
let (pair, current_date, valid_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
||||
.await?;
|
||||
let (pair, current_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||
|
||||
let verifier = generate_oauth_challenge();
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&verifier);
|
||||
let result = hasher.finalize();
|
||||
let result = sha2::Sha256::digest(&verifier);
|
||||
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
||||
|
||||
match sisu_authenticate(
|
||||
@@ -110,46 +107,15 @@ pub async fn login_begin(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((session_id, redirect_uri)) => Ok(MinecraftLoginFlow {
|
||||
verifier,
|
||||
challenge,
|
||||
session_id,
|
||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
}),
|
||||
Err(err) => {
|
||||
if !valid_date {
|
||||
let (pair, current_date, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(
|
||||
Utc::now(),
|
||||
false,
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let verifier = generate_oauth_challenge();
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&verifier);
|
||||
let result = hasher.finalize();
|
||||
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
||||
|
||||
let (session_id, redirect_uri) = sisu_authenticate(
|
||||
&pair.token.token,
|
||||
&challenge,
|
||||
&pair.key,
|
||||
current_date,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(MinecraftLoginFlow {
|
||||
verifier,
|
||||
challenge,
|
||||
session_id,
|
||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
})
|
||||
} else {
|
||||
Err(crate::ErrorKind::from(err).into())
|
||||
}
|
||||
Ok((session_id, redirect_uri)) => {
|
||||
return Ok(MinecraftLoginFlow {
|
||||
verifier,
|
||||
challenge,
|
||||
session_id,
|
||||
auth_request_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
});
|
||||
}
|
||||
Err(err) => return Err(crate::ErrorKind::from(err).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,9 +125,8 @@ pub async fn login_finish(
|
||||
flow: MinecraftLoginFlow,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<Credentials> {
|
||||
let (pair, _, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
||||
.await?;
|
||||
let (pair, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||
|
||||
let oauth_token = oauth_token(code, &flow.verifier).await?;
|
||||
let sisu_authorize = sisu_authorize(
|
||||
@@ -191,6 +156,7 @@ pub async fn login_finish(
|
||||
expires: oauth_token.date
|
||||
+ Duration::seconds(oauth_token.value.expires_in as i64),
|
||||
active: true,
|
||||
account_type: AccountType::Microsoft.as_lowercase_str(),
|
||||
};
|
||||
|
||||
// During login, we need to fetch the online profile at least once to get the
|
||||
@@ -229,6 +195,7 @@ pub async fn offline_auth(
|
||||
refresh_token: refresh_token,
|
||||
expires: Utc::now() + Duration::days(365 * 99),
|
||||
active: true,
|
||||
account_type: AccountType::Pirate.as_lowercase_str(),
|
||||
};
|
||||
|
||||
credentials.offline_profile = MinecraftProfile {
|
||||
@@ -242,6 +209,58 @@ pub async fn offline_auth(
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
// [AR] Feature
|
||||
#[tracing::instrument]
|
||||
pub async fn elyby_auth(
|
||||
uuid: Uuid,
|
||||
username: &str,
|
||||
access_token: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<Credentials> {
|
||||
let mut credentials = Credentials {
|
||||
offline_profile: MinecraftProfile::default(),
|
||||
access_token: access_token.to_string(),
|
||||
refresh_token: "null".to_string(),
|
||||
expires: Utc::now() + Duration::days(365 * 99),
|
||||
active: true,
|
||||
account_type: AccountType::ElyBy.as_lowercase_str(),
|
||||
};
|
||||
|
||||
credentials.offline_profile = MinecraftProfile {
|
||||
id: uuid,
|
||||
name: username.to_string(),
|
||||
..credentials.offline_profile
|
||||
};
|
||||
|
||||
credentials.upsert(exec).await?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// [AR] • Feature
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum AccountType {
|
||||
Unknown,
|
||||
Microsoft,
|
||||
Pirate,
|
||||
ElyBy,
|
||||
}
|
||||
|
||||
impl AccountType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AccountType::Unknown => "Unknown",
|
||||
AccountType::Microsoft => "Microsoft",
|
||||
AccountType::Pirate => "Pirate",
|
||||
AccountType::ElyBy => "ElyBy",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_lowercase_str(&self) -> String {
|
||||
self.as_str().to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Credentials {
|
||||
/// The offline profile of the user these credentials are for.
|
||||
@@ -255,6 +274,7 @@ pub struct Credentials {
|
||||
pub refresh_token: String,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub active: bool,
|
||||
pub account_type: String,
|
||||
}
|
||||
|
||||
/// An entry in the player profile cache, keyed by player UUID.
|
||||
@@ -296,10 +316,9 @@ impl Credentials {
|
||||
}
|
||||
|
||||
let oauth_token = oauth_refresh(&self.refresh_token).await?;
|
||||
let (pair, current_date, _) =
|
||||
let (pair, current_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(
|
||||
oauth_token.date,
|
||||
false,
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
@@ -480,7 +499,7 @@ impl Credentials {
|
||||
let res = sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
uuid, active, username, access_token, refresh_token, expires
|
||||
uuid, active, username, access_token, refresh_token, expires, account_type
|
||||
FROM minecraft_users
|
||||
WHERE active = TRUE
|
||||
"
|
||||
@@ -503,6 +522,7 @@ impl Credentials {
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
active: x.active == 1,
|
||||
account_type: x.account_type,
|
||||
};
|
||||
credentials.refresh(exec).await.ok();
|
||||
Some(credentials)
|
||||
@@ -517,7 +537,7 @@ impl Credentials {
|
||||
let res = sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
uuid, active, username, access_token, refresh_token, expires
|
||||
uuid, active, username, access_token, refresh_token, expires, account_type
|
||||
FROM minecraft_users
|
||||
"
|
||||
)
|
||||
@@ -537,6 +557,7 @@ impl Credentials {
|
||||
.single()
|
||||
.unwrap_or_else(Utc::now),
|
||||
active: x.active == 1,
|
||||
account_type: x.account_type,
|
||||
};
|
||||
|
||||
async move {
|
||||
@@ -572,14 +593,15 @@ impl Credentials {
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (uuid) DO UPDATE SET
|
||||
active = $2,
|
||||
username = $3,
|
||||
access_token = $4,
|
||||
refresh_token = $5,
|
||||
expires = $6
|
||||
expires = $6,
|
||||
account_type = $7
|
||||
",
|
||||
uuid,
|
||||
self.active,
|
||||
@@ -587,6 +609,7 @@ impl Credentials {
|
||||
self.access_token,
|
||||
self.refresh_token,
|
||||
expires,
|
||||
self.account_type,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -649,6 +672,7 @@ impl Serialize for Credentials {
|
||||
ser.serialize_field("refresh_token", &self.refresh_token)?;
|
||||
ser.serialize_field("expires", &self.expires)?;
|
||||
ser.serialize_field("active", &self.active)?;
|
||||
ser.serialize_field("account_type", &self.account_type)?;
|
||||
ser.end()
|
||||
}
|
||||
}
|
||||
@@ -662,21 +686,20 @@ impl DeviceTokenPair {
|
||||
#[tracing::instrument(skip(exec))]
|
||||
async fn refresh_and_get_device_token(
|
||||
current_date: DateTime<Utc>,
|
||||
force_generate: bool,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<(Self, DateTime<Utc>, bool)> {
|
||||
) -> crate::Result<(Self, DateTime<Utc>)> {
|
||||
let pair = Self::get(exec).await?;
|
||||
|
||||
if let Some(mut pair) = pair {
|
||||
if pair.token.not_after > Utc::now() && !force_generate {
|
||||
Ok((pair, current_date, false))
|
||||
if pair.token.not_after > current_date {
|
||||
Ok((pair, current_date))
|
||||
} else {
|
||||
let res = device_token(&pair.key, current_date).await?;
|
||||
|
||||
pair.token = res.value;
|
||||
pair.upsert(exec).await?;
|
||||
|
||||
Ok((pair, res.date, true))
|
||||
Ok((pair, res.date))
|
||||
}
|
||||
} else {
|
||||
let key = generate_key()?;
|
||||
@@ -689,7 +712,7 @@ impl DeviceTokenPair {
|
||||
|
||||
pair.upsert(exec).await?;
|
||||
|
||||
Ok((pair, res.date, true))
|
||||
Ok((pair, res.date))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,8 +810,8 @@ impl DeviceTokenPair {
|
||||
}
|
||||
|
||||
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
/* [AR] Fix
|
||||
* Weird visibility issue that didn't reproduce before
|
||||
@@ -871,7 +894,7 @@ async fn sisu_authenticate(
|
||||
"AppId": MICROSOFT_CLIENT_ID,
|
||||
"DeviceToken": token,
|
||||
"Offers": [
|
||||
REQUESTED_SCOPES
|
||||
REQUESTED_SCOPE
|
||||
],
|
||||
"Query": {
|
||||
"code_challenge": challenge,
|
||||
@@ -879,7 +902,7 @@ async fn sisu_authenticate(
|
||||
"state": generate_oauth_challenge(),
|
||||
"prompt": "select_account"
|
||||
},
|
||||
"RedirectUri": REDIRECT_URL,
|
||||
"RedirectUri": AUTH_REPLY_URL,
|
||||
"Sandbox": "RETAIL",
|
||||
"TokenType": "code",
|
||||
"TitleId": "1794566092",
|
||||
@@ -923,12 +946,12 @@ async fn oauth_token(
|
||||
verifier: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
query.insert("code", code);
|
||||
query.insert("code_verifier", verifier);
|
||||
query.insert("grant_type", "authorization_code");
|
||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||
query.insert("scope", REQUESTED_SCOPE);
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
@@ -972,11 +995,11 @@ async fn oauth_refresh(
|
||||
refresh_token: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
query.insert("refresh_token", refresh_token);
|
||||
query.insert("grant_type", "refresh_token");
|
||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||
query.insert("scope", REQUESTED_SCOPE);
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
@@ -1040,7 +1063,7 @@ async fn sisu_authorize(
|
||||
"/authorize",
|
||||
json!({
|
||||
"AccessToken": format!("t={access_token}"),
|
||||
"AppId": "00000000402b5328",
|
||||
"AppId": MICROSOFT_CLIENT_ID,
|
||||
"DeviceToken": device_token,
|
||||
"ProofKey": {
|
||||
"kty": "EC",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_URL};
|
||||
use crate::state::{CacheBehaviour, CachedEntry};
|
||||
use crate::util::fetch::{FetchSemaphore, fetch_advanced};
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
@@ -31,7 +30,7 @@ impl ModrinthCredentials {
|
||||
|
||||
let resp = fetch_advanced(
|
||||
Method::POST,
|
||||
&format!("{MODRINTH_API_URL}session/refresh"),
|
||||
concat!(env!("MODRINTH_API_URL"), "session/refresh"),
|
||||
None,
|
||||
None,
|
||||
Some(("Authorization", &*creds.session)),
|
||||
@@ -190,8 +189,8 @@ impl ModrinthCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_login_url() -> String {
|
||||
format!("{MODRINTH_URL}auth/sign-in?launcher=true")
|
||||
pub const fn get_login_url() -> &'static str {
|
||||
concat!(env!("MODRINTH_URL"), "auth/sign-in")
|
||||
}
|
||||
|
||||
pub async fn finish_login_flow(
|
||||
@@ -199,6 +198,12 @@ pub async fn finish_login_flow(
|
||||
semaphore: &FetchSemaphore,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
// The authorization code actually is the access token, since Labrinth doesn't
|
||||
// issue separate authorization codes. Therefore, this is equivalent to an
|
||||
// implicit OAuth grant flow, and no additional exchanging or finalization is
|
||||
// needed. TODO not do this for the reasons outlined at
|
||||
// https://oauth.net/2/grant-types/implicit/
|
||||
|
||||
let info = fetch_info(code, semaphore, exec).await?;
|
||||
|
||||
Ok(ModrinthCredentials {
|
||||
@@ -216,7 +221,7 @@ async fn fetch_info(
|
||||
) -> crate::Result<crate::state::cache::User> {
|
||||
let result = fetch_advanced(
|
||||
Method::GET,
|
||||
&format!("{MODRINTH_API_URL}user"),
|
||||
concat!(env!("MODRINTH_API_URL"), "user"),
|
||||
None,
|
||||
None,
|
||||
Some(("Authorization", token)),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Functions for fetching information from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::config::{MODRINTH_API_URL, MODRINTH_API_URL_V3};
|
||||
use crate::event::LoadingBarId;
|
||||
use crate::event::emit::emit_loading;
|
||||
use bytes::Bytes;
|
||||
@@ -84,8 +83,8 @@ pub async fn fetch_advanced(
|
||||
.as_ref()
|
||||
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
||||
&& (url.starts_with("https://cdn.modrinth.com")
|
||||
|| url.starts_with(MODRINTH_API_URL)
|
||||
|| url.starts_with(MODRINTH_API_URL_V3))
|
||||
|| url.starts_with(env!("MODRINTH_API_URL"))
|
||||
|| url.starts_with(env!("MODRINTH_API_URL_V3")))
|
||||
{
|
||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
///
|
||||
/// [AR] Feature
|
||||
///
|
||||
use crate::Result;
|
||||
use crate::api::update;
|
||||
use crate::state::db;
|
||||
///
|
||||
/// [AR] Feature Utils
|
||||
///
|
||||
/// Version: 0.1.1
|
||||
///
|
||||
use crate::{Result, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use tokio::io;
|
||||
use std::time::SystemTime;
|
||||
use tokio::{fs, io};
|
||||
|
||||
const PACKAGE_JSON_CONTENT: &str =
|
||||
// include_str!("../../../../apps/app-frontend/package.json");
|
||||
@@ -17,13 +21,135 @@ pub struct Launcher {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub fn read_package_json() -> io::Result<Launcher> {
|
||||
// Deserialize the content of package.json into a Launcher struct
|
||||
let launcher: Launcher = serde_json::from_str(PACKAGE_JSON_CONTENT)?;
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Artifact {
|
||||
path: Option<String>,
|
||||
sha1: Option<String>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Downloads {
|
||||
artifact: Option<Artifact>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Library {
|
||||
name: String,
|
||||
downloads: Option<Downloads>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct VersionJson {
|
||||
libraries: Vec<Library>,
|
||||
}
|
||||
|
||||
/// Deserialize the content of package.json into a Launcher struct
|
||||
pub fn read_package_json() -> io::Result<Launcher> {
|
||||
let launcher: Launcher = serde_json::from_str(PACKAGE_JSON_CONTENT)?;
|
||||
Ok(launcher)
|
||||
}
|
||||
|
||||
/// ### AR • Ely.by Injector
|
||||
/// Returns the PathBuf to the Ely.by AuthLib Injector
|
||||
/// If resource doesn't exist or outdated, it will be downloaded from Git Astralium.
|
||||
pub async fn get_or_download_elyby_injector() -> Result<PathBuf> {
|
||||
tracing::info!("[AR] • Initializing state for Ely.by AuthLib Injector...");
|
||||
let state = State::get().await?;
|
||||
let libraries_dir = state.directories.libraries_dir();
|
||||
|
||||
// Stores the local authlib injectors from `libraries/astralrinth/authlib_injectors/` directory.
|
||||
let mut local_authlib_injectors = Vec::new();
|
||||
|
||||
validate_astralrinth_library_dir(&libraries_dir, "authlib_injector/").await?;
|
||||
let astralrinth_dir = libraries_dir.join("astralrinth/");
|
||||
let authlib_injector_dir = astralrinth_dir.join("authlib_injector/");
|
||||
let mut authlib_injector_dir_data = fs::read_dir(&authlib_injector_dir).await?;
|
||||
|
||||
// Get all local authlib injectors
|
||||
while let Some(entry) = authlib_injector_dir_data.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||
if file_name.starts_with("authlib-injector") {
|
||||
let metadata = entry.metadata().await?;
|
||||
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
local_authlib_injectors.push((path.clone(), modified));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get information about latest authlib injector from remote repository
|
||||
let (asset_name, download_url) = match extract_elyby_authlib_metadata("authlib-injector").await {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
if let Some((local_path, _)) = local_authlib_injectors
|
||||
.iter()
|
||||
.max_by(|a, b| a.1.cmp(&b.1))
|
||||
{
|
||||
tracing::info!("[AR] • Found local AuthLib Injector(s):");
|
||||
for (path, time) in &local_authlib_injectors {
|
||||
tracing::info!("• {:?} (modified: {:?})", path.file_name().unwrap(), time);
|
||||
}
|
||||
tracing::warn!("[AR] • Failed to get latest AuthLib Injector from remote, using latest local version: {}", local_path.display());
|
||||
return Ok(local_path.clone());
|
||||
} else {
|
||||
tracing::error!("[AR] • Failed to get AuthLib Injector from remote and no local copy found.");
|
||||
return Err(crate::ErrorKind::NetworkErrorOccurred { error: format!("Failed to fetch authlib-injector metadata and no local version available: {}", err) }.as_error());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !local_authlib_injectors.is_empty() {
|
||||
local_authlib_injectors.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
tracing::info!("[AR] • Found local AuthLib Injector(s):");
|
||||
for (path, time) in &local_authlib_injectors {
|
||||
tracing::info!("• {:?} (modified: {:?})", path.file_name().unwrap(), time);
|
||||
}
|
||||
}
|
||||
|
||||
let remote_authlib_injector = if !asset_name.is_empty() {
|
||||
authlib_injector_dir.join(&asset_name)
|
||||
} else {
|
||||
return Err(crate::ErrorKind::ParseError { reason: "Asset name is empty from metadata".to_string() }.as_error());
|
||||
};
|
||||
|
||||
let latest_local_authlib_injector = local_authlib_injectors
|
||||
.first()
|
||||
.map(|(p, _)| p.clone());
|
||||
|
||||
let latest_local_authlib_injector_full_path_buf = match latest_local_authlib_injector {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
tracing::info!("[AR] • No local version found, will download from remote: {}", remote_authlib_injector.display());
|
||||
let bytes = fetch_bytes_from_url(download_url.as_str()).await?;
|
||||
write_file_to_libraries(&remote_authlib_injector.to_string_lossy(), &bytes).await?;
|
||||
tracing::info!("[AR] • Successfully saved AuthLib Injector to {}", remote_authlib_injector.display());
|
||||
return Ok(remote_authlib_injector);
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("[AR] • Remote Asset name: {}", asset_name);
|
||||
tracing::info!("[AR] • Remote Download URL: {}", download_url);
|
||||
tracing::info!("[AR] • Latest local AuthLib Injector: {}", latest_local_authlib_injector_full_path_buf.file_name().unwrap().to_string_lossy());
|
||||
tracing::info!("[AR] • Comparing local version {} with parsed remote version {}", latest_local_authlib_injector_full_path_buf.display(), remote_authlib_injector.display());
|
||||
|
||||
if remote_authlib_injector == latest_local_authlib_injector_full_path_buf {
|
||||
tracing::info!("[AR] • Remote version is the same as local version, using local copy.");
|
||||
return Ok(latest_local_authlib_injector_full_path_buf);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"[AR] • Doesn't exist or outdated, attempting to download latest AuthLib Injector from URL: {}",
|
||||
download_url
|
||||
);
|
||||
let bytes = fetch_bytes_from_url(download_url.as_str()).await?;
|
||||
write_file_to_libraries(&remote_authlib_injector.to_string_lossy(), &bytes).await?;
|
||||
tracing::info!("[AR] • Successfully saved AuthLib Injector to {}", remote_authlib_injector.display());
|
||||
return Ok(remote_authlib_injector);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### AR • Migration. Patch
|
||||
/// Applying migration fix for SQLite database.
|
||||
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
|
||||
tracing::info!("[AR] • Attempting to apply migration fix");
|
||||
let patched = db::apply_migration_fix(eol).await?;
|
||||
@@ -35,14 +161,19 @@ pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
|
||||
Ok(patched)
|
||||
}
|
||||
|
||||
pub async fn init_download(
|
||||
/// ### AR • Feature. Updater
|
||||
/// Initialize the update launcher.
|
||||
pub async fn init_update_launcher(
|
||||
download_url: &str,
|
||||
local_filename: &str,
|
||||
os_type: &str,
|
||||
auto_update_supported: bool,
|
||||
) -> Result<()> {
|
||||
println!("[AR] • Initialize downloading from • {:?}", download_url);
|
||||
println!("[AR] • Save local file name • {:?}", local_filename);
|
||||
tracing::info!("[AR] • Initialize downloading from • {:?}", download_url);
|
||||
tracing::info!("[AR] • Save local file name • {:?}", local_filename);
|
||||
tracing::info!("[AR] • OS type • {}", os_type);
|
||||
tracing::info!("[AR] • Auto update supported • {}", auto_update_supported);
|
||||
|
||||
if let Err(e) = update::get_resource(
|
||||
download_url,
|
||||
local_filename,
|
||||
@@ -61,3 +192,344 @@ pub async fn init_download(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### AR • AuthLib (Ely.by)
|
||||
/// Initializes the AuthLib patching process.
|
||||
///
|
||||
/// Returns `true` if the authlib patched successfully.
|
||||
pub async fn init_authlib_patching(
|
||||
minecraft_version: &str,
|
||||
is_mojang: bool,
|
||||
) -> Result<bool> {
|
||||
let minecraft_library_metadata =
|
||||
get_minecraft_library_metadata(minecraft_version).await?;
|
||||
// Parses the AuthLib version from string
|
||||
// Example output: "com.mojang:authlib:6.0.58" -> "6.0.58"
|
||||
let authlib_version = minecraft_library_metadata
|
||||
.name
|
||||
.split(':')
|
||||
.nth(2)
|
||||
.unwrap_or("unknown");
|
||||
let authlib_fullname_string = format!("authlib-{}.jar", authlib_version);
|
||||
let authlib_fullname_str = authlib_fullname_string.as_str();
|
||||
|
||||
tracing::info!(
|
||||
"[AR] • Attempting to download AuthLib -> {}.",
|
||||
authlib_fullname_string
|
||||
);
|
||||
|
||||
download_authlib(
|
||||
&minecraft_library_metadata,
|
||||
authlib_fullname_str,
|
||||
minecraft_version,
|
||||
is_mojang,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// ### AR • Universal Write (IO) Function.
|
||||
/// Validating the `astralrinth/{target_directory}/` directory exists inside the libraries/astralrinth directory.
|
||||
async fn validate_astralrinth_library_dir(
|
||||
libraries_dir: &PathBuf,
|
||||
validation_directory: &str
|
||||
) -> Result<()> {
|
||||
let astralrinth_path = libraries_dir.join(format!("astralrinth/{}", validation_directory));
|
||||
if !astralrinth_path.exists() {
|
||||
tokio::fs::create_dir_all(&astralrinth_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(
|
||||
"[AR] • Failed to create {} directory: {:?}",
|
||||
astralrinth_path.display(),
|
||||
e
|
||||
);
|
||||
crate::ErrorKind::IOErrorOccurred {
|
||||
error: format!(
|
||||
"Failed to create {} directory: {}",
|
||||
astralrinth_path.display(),
|
||||
e
|
||||
),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
tracing::info!(
|
||||
"[AR] • Created missing {} directory",
|
||||
astralrinth_path.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### AR • Universal Write (IO) Function
|
||||
/// Saves the downloaded bytes to the `libraries` directory using the given relative path.
|
||||
async fn write_file_to_libraries(
|
||||
relative_path: &str,
|
||||
bytes: &bytes::Bytes,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let output_path = state.directories.libraries_dir().join(relative_path);
|
||||
|
||||
fs::write(&output_path, bytes).await.map_err(|e| {
|
||||
tracing::error!("[AR] • Failed to save file: {:?}", e);
|
||||
crate::ErrorKind::IOErrorOccurred {
|
||||
error: format!("Failed to save file: {e}"),
|
||||
}
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
/// ### AR • AuthLib (Ely.by)
|
||||
/// Downloads the AuthLib file from Mojang libraries or Git Astralium services.
|
||||
async fn download_authlib(
|
||||
minecraft_library_metadata: &Library,
|
||||
authlib_fullname: &str,
|
||||
minecraft_version: &str,
|
||||
is_mojang: bool,
|
||||
) -> Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let (mut url, path) = extract_minecraft_local_download_info(
|
||||
minecraft_library_metadata,
|
||||
minecraft_version,
|
||||
)?;
|
||||
let full_path = state.directories.libraries_dir().join(path);
|
||||
|
||||
if !is_mojang {
|
||||
tracing::info!(
|
||||
"[AR] • Attempting to download AuthLib from Git Astralium"
|
||||
);
|
||||
(_, url) = extract_elyby_authlib_metadata(authlib_fullname).await?;
|
||||
}
|
||||
tracing::info!("[AR] • Downloading AuthLib from URL: {}", url);
|
||||
let bytes = fetch_bytes_from_url(&url).await?;
|
||||
tracing::info!("[AR] • Will save to path: {}", full_path.to_str().unwrap());
|
||||
write_file_to_libraries(full_path.to_str().unwrap(), &bytes).await?;
|
||||
tracing::info!("[AR] • Successfully saved AuthLib to {:?}", full_path);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// ### AR • AuthLib (Ely.by)
|
||||
/// Parses the ElyIntegration release JSON and returns the download URL for the given AuthLib version.
|
||||
async fn extract_elyby_authlib_metadata(
|
||||
authlib_fullname: &str,
|
||||
) -> Result<(String, String)> {
|
||||
const URL: &str = "https://git.astralium.su/api/v1/repos/didirus/ElyIntegration/releases/latest";
|
||||
|
||||
let response = reqwest::get(URL).await.map_err(|e| {
|
||||
tracing::error!(
|
||||
"[AR] • Failed to fetch ElyIntegration release JSON: {:?}",
|
||||
e
|
||||
);
|
||||
crate::ErrorKind::NetworkErrorOccurred {
|
||||
error: format!(
|
||||
"Failed to fetch ElyIntegration release JSON: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let json: serde_json::Value = response.json().await.map_err(|e| {
|
||||
tracing::error!("[AR] • Failed to parse ElyIntegration JSON: {:?}", e);
|
||||
crate::ErrorKind::ParseError {
|
||||
reason: format!("Failed to parse ElyIntegration JSON: {}", e),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let assets =
|
||||
json.get("assets")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::ParseError {
|
||||
reason: "Missing 'assets' array".into(),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let asset = assets
|
||||
.iter()
|
||||
.find(|a| {
|
||||
a.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n.contains(authlib_fullname))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::ParseError {
|
||||
reason: format!(
|
||||
"No matching asset for {} in ElyIntegration JSON response.",
|
||||
authlib_fullname
|
||||
),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let download_url = asset
|
||||
.get("browser_download_url")
|
||||
.and_then(|u| u.as_str())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::ParseError {
|
||||
reason: "Missing 'browser_download_url'".into(),
|
||||
}
|
||||
.as_error()
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let asset_name = asset
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::ParseError {
|
||||
reason: "Missing 'name'".into(),
|
||||
}
|
||||
.as_error()
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
Ok((asset_name, download_url))
|
||||
}
|
||||
|
||||
/// ### AR • AuthLib (Ely.by)
|
||||
/// Extracts the artifact URL and Path from the library structure.
|
||||
///
|
||||
/// Returns a tuple of references to the URL and path strings,
|
||||
/// or an error if the required metadata is missing.
|
||||
fn extract_minecraft_local_download_info(
|
||||
minecraft_library_metadata: &Library,
|
||||
minecraft_version: &str,
|
||||
) -> Result<(String, String)> {
|
||||
let artifact = minecraft_library_metadata
|
||||
.downloads
|
||||
.as_ref()
|
||||
.and_then(|d| d.artifact.as_ref())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||
minecraft_version: minecraft_version.to_string(),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let url = artifact.url.clone().ok_or_else(|| {
|
||||
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||
minecraft_version: minecraft_version.to_string(),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let path = artifact.path.clone().ok_or_else(|| {
|
||||
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||
minecraft_version: minecraft_version.to_string(),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
Ok((url, path))
|
||||
}
|
||||
|
||||
/// ### AR • Universal Fetch Bytes (IO)
|
||||
/// Downloads bytes from the provided URL with a 15 second timeout.
|
||||
async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
|
||||
// Create client instance with request timeout.
|
||||
let client = reqwest::Client::new();
|
||||
const TIMEOUT_SECONDS: u64 = 15;
|
||||
|
||||
let response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(TIMEOUT_SECONDS),
|
||||
client.get(url).send(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
tracing::error!(
|
||||
"[AR] • Download timed out after {} seconds",
|
||||
TIMEOUT_SECONDS
|
||||
);
|
||||
crate::ErrorKind::NetworkErrorOccurred {
|
||||
error: format!(
|
||||
"Download timed out after {TIMEOUT_SECONDS} seconds"
|
||||
)
|
||||
.to_string(),
|
||||
}
|
||||
.as_error()
|
||||
})?
|
||||
.map_err(|e| {
|
||||
tracing::error!("[AR] • Request error: {:?}", e);
|
||||
crate::ErrorKind::NetworkErrorOccurred {
|
||||
error: format!("Request error: {e}"),
|
||||
}
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().to_string();
|
||||
tracing::error!("[AR] • Failed to download file: HTTP {}", status);
|
||||
return Err(crate::ErrorKind::NetworkErrorOccurred {
|
||||
error: format!("Failed to download file: HTTP {status}"),
|
||||
}
|
||||
.as_error());
|
||||
}
|
||||
|
||||
response.bytes().await.map_err(|e| {
|
||||
tracing::error!("[AR] • Failed to read response bytes: {:?}", e);
|
||||
crate::ErrorKind::NetworkErrorOccurred {
|
||||
error: format!("Failed to read response bytes: {e}"),
|
||||
}
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
/// ### AR • AuthLib (Ely.by)
|
||||
/// Gets the Minecraft library metadata from the local libraries directory.
|
||||
async fn get_minecraft_library_metadata(
|
||||
minecraft_version: &str,
|
||||
) -> Result<Library> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let path = state
|
||||
.directories
|
||||
.version_dir(minecraft_version)
|
||||
.join(format!("{}.json", minecraft_version));
|
||||
if !path.exists() {
|
||||
tracing::error!("[AR] • File not found: {:#?}", path);
|
||||
return Err(crate::ErrorKind::InvalidMinecraftVersion {
|
||||
minecraft_version: minecraft_version.to_string(),
|
||||
}
|
||||
.as_error());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&path).await?;
|
||||
let version_data: VersionJson = serde_json::from_str(&content)?;
|
||||
|
||||
for lib in version_data.libraries {
|
||||
if lib.name.contains("com.mojang:authlib") {
|
||||
if let Some(downloads) = &lib.downloads {
|
||||
if let Some(artifact) = &downloads.artifact {
|
||||
if artifact.path.is_some()
|
||||
&& artifact.url.is_some()
|
||||
&& artifact.sha1.is_some()
|
||||
{
|
||||
tracing::info!("[AR] • Found AuthLib: {}", lib.name);
|
||||
tracing::info!(
|
||||
"[AR] • Path: {}",
|
||||
artifact.path.as_ref().unwrap()
|
||||
);
|
||||
tracing::info!(
|
||||
"[AR] • URL: {}",
|
||||
artifact.url.as_ref().unwrap()
|
||||
);
|
||||
tracing::info!(
|
||||
"[AR] • SHA1: {}",
|
||||
artifact.sha1.as_ref().unwrap()
|
||||
);
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(crate::ErrorKind::MinecraftMetadataNotFound {
|
||||
minecraft_version: minecraft_version.to_string(),
|
||||
}
|
||||
.as_error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user