format the rust :3

This commit is contained in:
Pauline 2024-03-23 09:42:40 +01:00
parent b2f459478a
commit 7d6fbf8fd8
No known key found for this signature in database
GPG key ID: 722BB181973296AE
16 changed files with 887 additions and 716 deletions

View file

@ -1,3 +1,3 @@
fn main() {
prisma_client_rust_cli::run();
}
prisma_client_rust_cli::run();
}

View file

@ -1,2 +1,2 @@
#[allow(warnings, unused)]
pub mod prisma;
pub mod prisma;

View file

@ -11,7 +11,7 @@
"serve": "vite preview",
"tauri": "tauri",
"lint": "eslint . --cache",
"format": "pnpm lint --fix",
"format": "pnpm lint --fix && cargo fmt",
"prisma": "cd src-tauri && cargo prisma"
},
"dependencies": {

View file

@ -1,13 +1,12 @@
use tauri_build::{Attributes, InlinedPlugin};
fn main() {
let _ = tauri_build::try_build(Attributes::new()
.plugin("auth", InlinedPlugin::new().commands(&[
"login_msa"
]))
.plugin("game", InlinedPlugin::new().commands(&[
"launch_game",
"set_selected_client"
]))
);
let _ = tauri_build::try_build(
Attributes::new()
.plugin("auth", InlinedPlugin::new().commands(&["login_msa"]))
.plugin(
"game",
InlinedPlugin::new().commands(&["launch_game", "set_selected_client"]),
),
);
}

View file

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"
provider = "sqlite"

View file

@ -1,4 +1,7 @@
use std::{io::{Read, Write}, net::{SocketAddr, TcpListener, TcpStream}};
use std::{
io::{Read, Write},
net::{SocketAddr, TcpListener, TcpStream},
};
use serde_json::json;
use tauri::{AppHandle, Runtime};
@ -14,197 +17,246 @@ use super::{Account, AuthenticationError, AuthenticationMethod};
#[derive(Debug, Error)]
pub enum MicrosoftAuthError {
#[error("failed to get Minecraft access token")]
MSAccessToken,
#[error("failed to get XSTS token")]
XSTSToken,
#[error("failed to get XBL token")]
XBLToken,
#[error("failed to get XBL user hash")]
XBLUserHash,
#[error("this account doesn't have an xbox account, please sign in through minecraft.net and try again")]
NoXBoxAccount,
#[error("this account is from a country where xbox live is not available")]
XBoxLiveBlocked,
#[error("this account needs adult verification on the xbox page")]
AdultVerification,
#[error("this account is under 18, please add this account to a family by an adult and try again")]
ChildAccount,
#[error("unknown xbox live error: {0}")]
UnknownError(u64)
#[error("failed to get Minecraft access token")]
MSAccessToken,
#[error("failed to get XSTS token")]
XSTSToken,
#[error("failed to get XBL token")]
XBLToken,
#[error("failed to get XBL user hash")]
XBLUserHash,
#[error("this account doesn't have an xbox account, sign in through minecraft.net and try again")]
NoXBoxAccount,
#[error("this account is from a country where xbox live is not available")]
XBoxLiveBlocked,
#[error("this account needs adult verification on the xbox page")]
AdultVerification,
#[error("this account is a child, ask an adult to add this account to a Family")]
ChildAccount,
#[error("unknown xbox live error: {0}")]
UnknownError(u64),
}
pub(super) struct MicrosoftAuthenticationMethod;
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> PolyResult<Account>
where F: Fn(String, u8, bool) -> () {
stage("Authenticating with Microsoft".into(), 0, false);
let msa_code: String = msa_code(handle).await?;
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> PolyResult<Account>
where
F: Fn(String, u8, bool) -> (),
{
stage("Authenticating with Microsoft".into(), 0, false);
let msa_code: String = msa_code(handle).await?;
stage("Authenticating with Microsoft".into(), 1, false);
let msa_token: String = msa_code_to_token(msa_code).await?;
stage("Authenticating with Microsoft".into(), 1, false);
let msa_token: String = msa_code_to_token(msa_code).await?;
stage("Authenticating with Xbox Live".into(), 2, false);
let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?;
stage("Authenticating with Xbox Live".into(), 2, false);
let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?;
stage("Retrieving XSTS token".into(), 3, false);
let xsts_token: String = auth_xsts(xbl_token).await?;
stage("Retrieving XSTS token".into(), 3, false);
let xsts_token: String = auth_xsts(xbl_token).await?;
stage("Authenticating with Minecraft".into(), 4, false);
let access_token: String = auth_minecraft(xsts_token, user_hash).await?;
stage("Authenticating with Minecraft".into(), 4, false);
let access_token: String = auth_minecraft(xsts_token, user_hash).await?;
stage("Retrieving Minecraft profile".into(), 5, true);
let account: Account = Self::get_profile(access_token).await?;
stage("Retrieving Minecraft profile".into(), 5, true);
let account: Account = Self::get_profile(access_token).await?;
Ok(account)
}
Ok(account)
}
}
async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult<String> {
let response = create_client()?
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
}))
.send()
.await?;
let response = create_client()?
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
}))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
let access_token = match response.get("access_token") {
Some(token) => token.as_str().unwrap(),
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::MSAccessToken)))
};
let response = response.json::<serde_json::Value>().await?;
let access_token = match response.get("access_token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::MSAccessToken,
)))
}
};
Ok(access_token.to_string())
Ok(access_token.to_string())
}
async fn auth_xsts(token: String) -> PolyResult<String> {
let response = create_client()?
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
let response = create_client()?
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
if response.status().as_u16() == 401 {
let error_code = response.json::<serde_json::Value>().await?["XErr"].as_u64().unwrap();
if response.status().as_u16() == 401 {
let error_code = response.json::<serde_json::Value>().await?["XErr"]
.as_u64()
.unwrap();
match error_code {
2148916233 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::NoXBoxAccount))),
2148916235 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBoxLiveBlocked))),
2148916236 | 2148916237 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::AdultVerification))),
2148916238 => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::ChildAccount))),
_ => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::UnknownError(error_code)))),
}
}
match error_code {
2148916233 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::NoXBoxAccount,
)))
}
2148916235 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBoxLiveBlocked,
)))
}
2148916236 | 2148916237 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::AdultVerification,
)))
}
2148916238 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::ChildAccount,
)))
}
_ => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::UnknownError(error_code),
)))
}
}
}
let response = response.json::<serde_json::Value>().await?;
let xsts_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XSTSToken)))
};
let response = response.json::<serde_json::Value>().await?;
let xsts_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XSTSToken,
)))
}
};
Ok(xsts_token.to_string())
Ok(xsts_token.to_string())
}
async fn auth_xbl(code: String) -> PolyResult<(String, String)> {
let response = create_client()?
.post("https://user.auth.xboxlive.com/user/authenticate")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": &format!("d={}", code)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
.await?;
let response = create_client()?
.post("https://user.auth.xboxlive.com/user/authenticate")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": &format!("d={}", code)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
let xbl_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLToken)))
};
let response = response.json::<serde_json::Value>().await?;
let xbl_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBLToken,
)))
}
};
let user_hash = match response.get("DisplayClaims").and_then(|claims| claims.get("xui").and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs")))) {
Some(hash) => hash.as_str().unwrap(),
None => return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(MicrosoftAuthError::XBLUserHash)))
};
let user_hash = match response.get("DisplayClaims").and_then(|claims| {
claims
.get("xui")
.and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs")))
}) {
Some(hash) => hash.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBLUserHash,
)))
}
};
Ok((xbl_token.to_string(), user_hash.to_string()))
Ok((xbl_token.to_string(), user_hash.to_string()))
}
async fn msa_code_to_token(code: String) -> PolyResult<String> {
let token = create_client()?
.post("https://login.live.com/oauth20_token.srf")
.form(&[
("client_id", CLIENT_ID),
("redirect_uri", &format!("http://localhost:{}/", PORT)),
("code", &code),
("grant_type", "authorization_code")
])
.send()
.await?
.json::<serde_json::Value>()
.await?;
let token = create_client()?
.post("https://login.live.com/oauth20_token.srf")
.form(&[
("client_id", CLIENT_ID),
("redirect_uri", &format!("http://localhost:{}/", PORT)),
("code", &code),
("grant_type", "authorization_code"),
])
.send()
.await?
.json::<serde_json::Value>()
.await?;
Ok(token["access_token"].as_str().unwrap().to_string())
Ok(token["access_token"].as_str().unwrap().to_string())
}
const PORT: u16 = 13523;
async fn msa_code<R: Runtime>(handle: &AppHandle<R>) -> PolyResult<String> {
let url = format!(
let url = format!(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent",
CLIENT_ID,
CLIENT_ID,
PORT
);
handle.shell().open(url, None)?;
handle.shell().open(url, None)?;
let mut token: String = String::new();
let mut token: String = String::new();
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], PORT)))?;
println!("Started local server for MSA on port {}", PORT);
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], PORT)))?;
println!("Started local server for MSA on port {}", PORT);
for conn in listener.incoming() {
match conn {
Err(err) => return Err(err.into()),
Ok(stream) => {
if let Some(code) = handle_connection(stream) {
token = code;
break;
}
}
}
}
for conn in listener.incoming() {
match conn {
Err(err) => return Err(err.into()),
Ok(stream) => {
if let Some(code) = handle_connection(stream) {
token = code;
break;
}
}
}
}
Ok(token)
Ok(token)
}
fn handle_connection(mut stream: TcpStream) -> Option<String> {
let mut buffer = [0; 512];
if let Err(err) = stream.read(&mut buffer) {
eprintln!("Failed to read from stream: {}", err);
return None;
}
let mut buffer = [0; 512];
if let Err(err) = stream.read(&mut buffer) {
eprintln!("Failed to read from stream: {}", err);
return None;
}
let request = String::from_utf8_lossy(&buffer[..]);
let url = request.split_whitespace().nth(1)?;
let code = url.split('?').nth(1)?.split('&').find(|s| s.starts_with("code="))?.split_once('=').unwrap().1;
let response = r"
let request = String::from_utf8_lossy(&buffer[..]);
let url = request.split_whitespace().nth(1)?;
let code = url
.split('?')
.nth(1)?
.split('&')
.find(|s| s.starts_with("code="))?
.split_once('=')
.unwrap()
.1;
let response = r"
<!DOCTYPE html>
<html>
<head>
@ -239,14 +291,16 @@ fn handle_connection(mut stream: TcpStream) -> Option<String> {
</html>
";
stream.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
response.len(),
response
)
.as_bytes(),
).unwrap();
stream
.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
response.len(),
response
)
.as_bytes(),
)
.unwrap();
Some(code.to_string())
Some(code.to_string())
}

View file

@ -1,8 +1,8 @@
use anyhow::anyhow;
use thiserror::Error;
use serde::{Deserialize, Serialize};
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
use tauri_plugin_http::reqwest::Client;
use thiserror::Error;
use crate::{PolyError, PolyResult};
@ -12,64 +12,65 @@ mod microsoft_auth;
#[derive(Error, Debug)]
pub enum AuthenticationError {
#[error(transparent)]
MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError)
#[error(transparent)]
MicrosoftError(#[from] microsoft_auth::MicrosoftAuthError),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Account {
pub username: String,
pub uuid: String,
pub skins: Vec<AccountSkin>,
pub username: String,
pub uuid: String,
pub skins: Vec<AccountSkin>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AccountSkin {
pub id: String,
pub state: String,
pub url: String,
pub variant: String,
pub id: String,
pub state: String,
pub url: String,
pub variant: String,
}
#[allow(async_fn_in_trait)]
pub trait AuthenticationMethod {
/// Authenticate with a given method. Stage is a function that takes a string and a u8.
/// The string is the message to display to the user, and the u8 is the progress of the authentication.
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> PolyResult<Account>
where F: Fn(String, u8, bool) -> ();
/// Authenticate with a given method. Stage is a function that takes a string and a u8.
/// The string is the message to display to the user, and the u8 is the progress of the authentication.
async fn auth<R: Runtime, F>(handle: &AppHandle<R>, stage: F) -> PolyResult<Account>
where
F: Fn(String, u8, bool) -> ();
async fn get_profile(access_token: String) -> PolyResult<Account> {
let response = Client::new()
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
if let Some(error) = response.get("error") {
return Err(PolyError::AnyhowError(anyhow!(error.to_string())));
}
let account = serde_json::from_value::<Account>(response)?;
Ok(account)
}
async fn get_profile(access_token: String) -> PolyResult<Account> {
let response = Client::new()
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
if let Some(error) = response.get("error") {
return Err(PolyError::AnyhowError(anyhow!(error.to_string())));
}
let account = serde_json::from_value::<Account>(response)?;
Ok(account)
}
}
// Plugin for MSA
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("auth")
.invoke_handler(tauri::generate_handler![
login_msa
])
.build()
tauri::plugin::Builder::new("auth")
.invoke_handler(tauri::generate_handler![login_msa])
.build()
}
#[tauri::command]
async fn login_msa<R: tauri::Runtime>(app: tauri::AppHandle<R>) -> Result<Account, String> {
match MicrosoftAuthenticationMethod::auth(&app, |status, stage, was_last| {
let _ = app.emit("msa:status", (status, stage, was_last));
}).await {
Ok(account) => Ok(account),
Err(err) => Err(err.to_string())
}
match MicrosoftAuthenticationMethod::auth(&app, |status, stage, was_last| {
let _ = app.emit("msa:status", (status, stage, was_last));
})
.await
{
Ok(account) => Ok(account),
Err(err) => Err(err.to_string()),
}
}

View file

@ -7,9 +7,15 @@ use tauri::{AppHandle, Manager};
use thiserror::Error;
use uuid::Uuid;
use crate::{utils::{file, http}, PolyError, PolyResult};
use crate::{
utils::{file, http},
PolyError, PolyResult,
};
use super::{clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest}, JavaVersion};
use super::{
clients::vanilla::{vanilla_manifest, VanillaClient, VanillaManifest},
JavaVersion,
};
#[macro_export]
macro_rules! create_manifest {
@ -36,178 +42,228 @@ macro_rules! create_client {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "manifest")]
pub enum GameClientType {
Vanilla(VanillaManifest),
// Forge(ForgeManifest),
Vanilla(VanillaManifest),
// Forge(ForgeManifest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameClientDetails {
pub uuid: Uuid,
pub name: String,
pub version: String,
pub main_class: String,
pub java_version: JavaVersion,
pub startup_args: Vec<String>,
pub client_type: GameClientType,
pub uuid: Uuid,
pub name: String,
pub version: String,
pub main_class: String,
pub java_version: JavaVersion,
pub startup_args: Vec<String>,
pub client_type: GameClientType,
}
pub fn get_game_client(handle: AppHandle, details: GameClientDetails) -> Box<dyn GameClient> {
Box::new(match details.client_type {
GameClientType::Vanilla(_) => VanillaClient::new(handle, details),
// GameClientType::Forge(_) => Box::new(ForgeClient::new(handle, details)),
})
Box::new(match details.client_type {
GameClientType::Vanilla(_) => VanillaClient::new(handle, details),
// GameClientType::Forge(_) => Box::new(ForgeClient::new(handle, details)),
})
}
#[async_trait]
pub trait GameClient: Send + Sync {
fn new(handle: AppHandle, details: GameClientDetails) -> Self where Self: Sized;
fn new(handle: AppHandle, details: GameClientDetails) -> Self
where
Self: Sized;
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>> where Self: Sized;
async fn get_minecraft_manifest(version: String) -> PolyResult<vanilla_manifest::MinecraftManifest> where Self: Sized;
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>>
where
Self: Sized;
async fn get_minecraft_manifest(
version: String,
) -> PolyResult<vanilla_manifest::MinecraftManifest>
where
Self: Sized;
// async fn get_details_from_version(version: String) -> Result<GameClientDetails, Box<dyn Error>> where Self: Sized;
// async fn get_details_from_version(version: String) -> Result<GameClientDetails, Box<dyn Error>> where Self: Sized;
fn get_handle(&self) -> &AppHandle;
fn get_details(&self) -> &GameClientDetails;
fn get_client(&self) -> &GameClientType {
&self.get_details().client_type
}
async fn launch(&self) -> PolyResult<()>;
async fn setup(&self) -> PolyResult<()>;
fn get_handle(&self) -> &AppHandle;
fn get_details(&self) -> &GameClientDetails;
fn get_client(&self) -> &GameClientType {
&self.get_details().client_type
}
async fn download_java(&self) -> PolyResult<PathBuf> {
let config_dir = match self.get_handle().path().app_config_dir() {
Ok(dir) => dir,
// SURELY THERES A BETTER WAY TO DO THIS IDK LOLL
Err(err) => return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(err.to_string())))
};
let java_dir = config_dir.join("java");
if !java_dir.exists() {
match fs::create_dir_all(&java_dir) {
Ok(_) => (),
Err(err) => return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied(err.to_string())))
};
}
let java_version = &self.get_details().java_version;
let os = env::consts::OS;
let archive_type = match os {
"windows" => "zip",
"macos" => "tar.gz",
"linux" => "tar.gz",
_ => return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(os.to_string())))
};
let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type);
let archive = java_dir.join(&archive_name);
let dest = java_dir.join(format!("zulu-{}-{}", java_version.to_string(), get_arch()));
if archive.exists() && dest.exists() {
let _ = fs::remove_file(archive.as_path());
} else if archive.exists() && !dest.exists() {
extract(&archive, &dest)?;
} else if !archive.exists() && !dest.exists() {
download(java_version, os, archive_type, &archive).await?;
extract(&archive, &dest)?;
}
if let Ok(mut files) = fs::read_dir(&dest) {
let file = match files.nth(0) {
Some(file) => file.unwrap().path(),
None => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("unable to get the java executable file".to_string())))
};
return Ok(file.join("bin").join("java"));
}
Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("unable to download the java executable file".to_string())))
}
async fn launch(&self) -> PolyResult<()>;
async fn setup(&self) -> PolyResult<()>;
async fn download_java(&self) -> PolyResult<PathBuf> {
let config_dir = match self.get_handle().path().app_config_dir() {
Ok(dir) => dir,
// SURELY THERES A BETTER WAY TO DO THIS IDK LOLL
Err(err) => {
return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(
err.to_string(),
)))
}
};
let java_dir = config_dir.join("java");
if !java_dir.exists() {
match fs::create_dir_all(&java_dir) {
Ok(_) => (),
Err(err) => {
return Err(PolyError::JavaError(JavaDownloadError::PermissionDenied(
err.to_string(),
)))
}
};
}
let java_version = &self.get_details().java_version;
let os = env::consts::OS;
let archive_type = match os {
"windows" => "zip",
"macos" => "tar.gz",
"linux" => "tar.gz",
_ => {
return Err(PolyError::JavaError(JavaDownloadError::UnsupportedOS(
os.to_string(),
)))
}
};
let archive_name = format!("zulu-{}.{}", java_version.to_string(), archive_type);
let archive = java_dir.join(&archive_name);
let dest = java_dir.join(format!("zulu-{}-{}", java_version.to_string(), get_arch()));
if archive.exists() && dest.exists() {
let _ = fs::remove_file(archive.as_path());
} else if archive.exists() && !dest.exists() {
extract(&archive, &dest)?;
} else if !archive.exists() && !dest.exists() {
download(java_version, os, archive_type, &archive).await?;
extract(&archive, &dest)?;
}
if let Ok(mut files) = fs::read_dir(&dest) {
let file = match files.nth(0) {
Some(file) => file.unwrap().path(),
None => {
return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(
"unable to get the java executable file".to_string(),
)))
}
};
return Ok(file.join("bin").join("java"));
}
Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(
"unable to download the java executable file".to_string(),
)))
}
}
#[async_trait]
pub trait ModdedGameClient: GameClient {
async fn install_mods(&self) -> PolyResult<()>;
async fn install_mods(&self) -> PolyResult<()>;
}
#[derive(Debug, Error)]
pub enum JavaDownloadError {
#[error("failed to find a valid java version: {0}")]
NoJavaVersionFound(String),
#[error("failed creating a java directory (try running at an elevated permission): {0}")]
PermissionDenied(String),
#[error("unable to match your operating system ({0}) with a valid java version")]
UnsupportedOS(String),
#[error("unable to match your architecture ({0}) with a valid java version")]
UnsupportedArch(String),
#[error("failed to extract the java executable: {0}")]
ExtractError(String),
#[error("failed to download the java executable")]
DownloadError(String),
#[error("failed to find a valid java version: {0}")]
NoJavaVersionFound(String),
#[error("failed creating a java directory (try running at an elevated permission): {0}")]
PermissionDenied(String),
#[error("unable to match your operating system ({0}) with a valid java version")]
UnsupportedOS(String),
#[error("unable to match your architecture ({0}) with a valid java version")]
UnsupportedArch(String),
#[error("failed to extract the java executable: {0}")]
ExtractError(String),
#[error("failed to download the java executable")]
DownloadError(String),
}
fn get_arch() -> String {
let arch = env::consts::ARCH;
match arch {
"x86" => "x86",
"x86_64" => "x64",
"arm" => "aarch32",
"aarch64" => "aarch64",
_ => "unsupported"
}.to_string()
let arch = env::consts::ARCH;
match arch {
"x86" => "x86",
"x86_64" => "x64",
"arm" => "aarch32",
"aarch64" => "aarch64",
_ => "unsupported",
}
.to_string()
}
async fn get_java_versions(java_version: &JavaVersion, os: &str, archive_type: &str) -> PolyResult<Value> {
let url = format!("
async fn get_java_versions(
java_version: &JavaVersion,
os: &str,
archive_type: &str,
) -> PolyResult<Value> {
let url = format!(
"
https://api.azul.com/metadata/v1/zulu/packages/\
?java_version={}\
&os={}\
&arch={}\
&archive_type={}\
&java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true",
java_version.to_string(),
os,
get_arch(),
archive_type
);
&java_package_type=jre&javafx_bundled=true&release_status=ga&latest=true",
java_version.to_string(),
os,
get_arch(),
archive_type
);
let response = match http::create_client()?.get(&url).send().await {
Ok(response) => match response.json::<serde_json::Value>().await {
Ok(json) => json,
Err(err) => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(err.to_string())))
},
Err(err) => return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(err.to_string())))
};
let response = match http::create_client()?.get(&url).send().await {
Ok(response) => match response.json::<serde_json::Value>().await {
Ok(json) => json,
Err(err) => {
return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(
err.to_string(),
)))
}
},
Err(err) => {
return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(
err.to_string(),
)))
}
};
if !response.is_array() || response.as_array().unwrap().is_empty() {
return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound("didn't get the expected java api result".to_string())));
}
if !response.is_array() || response.as_array().unwrap().is_empty() {
return Err(PolyError::JavaError(JavaDownloadError::NoJavaVersionFound(
"didn't get the expected java api result".to_string(),
)));
}
Ok(response)
Ok(response)
}
async fn download(java_version: &JavaVersion, os: &str, archive_type: &str, archive: &PathBuf) -> PolyResult<()> {
let response = get_java_versions(java_version, os, archive_type).await?;
let latest = response.as_array().unwrap().first().unwrap();
let download_url = latest.get("download_url").unwrap().as_str().unwrap();
async fn download(
java_version: &JavaVersion,
os: &str,
archive_type: &str,
archive: &PathBuf,
) -> PolyResult<()> {
let response = get_java_versions(java_version, os, archive_type).await?;
let latest = response.as_array().unwrap().first().unwrap();
let download_url = latest.get("download_url").unwrap().as_str().unwrap();
if let Err(err) = http::download_file(download_url, archive.as_path()).await {
// eprintln!("{}", err);
return Err(PolyError::JavaError(JavaDownloadError::DownloadError(err.to_string())));
};
Ok(())
if let Err(err) = http::download_file(download_url, archive.as_path()).await {
// eprintln!("{}", err);
return Err(PolyError::JavaError(JavaDownloadError::DownloadError(
err.to_string(),
)));
};
Ok(())
}
fn extract(archive: &PathBuf, dest: &PathBuf) -> PolyResult<()> {
if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) {
// eprintln!("{}", err);
let _ = fs::remove_file(dest.as_path());
return Err(PolyError::JavaError(JavaDownloadError::ExtractError(err.to_string())));
}
if let Err(err) = file::extract_archive(archive.as_path(), dest.as_path()) {
let _ = fs::remove_file(dest.as_path());
return Err(PolyError::JavaError(JavaDownloadError::ExtractError(
err.to_string(),
)));
}
let _ = fs::remove_file(archive.as_path());
Ok(())
let _ = fs::remove_file(archive.as_path());
Ok(())
}

View file

@ -1 +1 @@
pub mod vanilla;
pub mod vanilla;

View file

@ -6,7 +6,12 @@ use serde_json::Value;
use tauri::{AppHandle, Manager};
use tokio::io::{stdout, AsyncWriteExt};
use crate::{create_client, create_manifest, game::client::{GameClient, GameClientDetails, GameClientType}, utils::http, PolyError, PolyResult};
use crate::{
create_client, create_manifest,
game::client::{GameClient, GameClientDetails, GameClientType},
utils::http,
PolyError, PolyResult,
};
create_manifest!(VanillaManifest {
// minecraft: vanilla_manifest::MinecraftManifest
@ -16,303 +21,336 @@ create_client!(VanillaClient {});
#[async_trait]
impl GameClient for VanillaClient {
fn new(handle: AppHandle, details: GameClientDetails) -> Self {
Self {
handle: handle.clone(),
details
}
}
fn new(handle: AppHandle, details: GameClientDetails) -> Self {
Self {
handle: handle.clone(),
details,
}
}
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>> {
const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
let request = http::create_client()?.get(URL).send().await.map_err(|err| PolyError::HTTPError(err))?;
async fn get_all_version_urls() -> PolyResult<HashMap<String, String>> {
const URL: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
let request = http::create_client()?
.get(URL)
.send()
.await
.map_err(|err| PolyError::HTTPError(err))?;
let json = request.json::<Value>().await.map_err(|err| PolyError::HTTPError(err))?;
let json = request
.json::<Value>()
.await
.map_err(|err| PolyError::HTTPError(err))?;
let versions = json.get("versions").ok_or(PolyError::AnyhowError(anyhow!("failed to get versions object")))?
.as_array().ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?;
let versions = json
.get("versions")
.ok_or(PolyError::AnyhowError(anyhow!(
"failed to get versions object"
)))?
.as_array()
.ok_or(PolyError::AnyhowError(anyhow!("invalid versions object")))?;
let mut map = HashMap::new();
let mut map = HashMap::new();
for version in versions {
let version = version.as_object().ok_or("Invalid version object")?;
for version in versions {
let version = version.as_object().ok_or("Invalid version object")?;
let id = version.get("id").ok_or("No id object")?
.as_str().ok_or("Invalid id object")?;
let url = version.get("url").ok_or("No url object")?
.as_str().ok_or("Invalid url object")?;
let id = version
.get("id")
.ok_or("No id object")?
.as_str()
.ok_or("Invalid id object")?;
let url = version
.get("url")
.ok_or("No url object")?
.as_str()
.ok_or("Invalid url object")?;
map.insert(id.to_string(), url.to_string());
}
map.insert(id.to_string(), url.to_string());
}
Ok(map)
}
async fn get_minecraft_manifest(version_url: String) -> Result<vanilla_manifest::MinecraftManifest, Box<dyn Error>> {
let request = http::create_client().get(version_url).send().await?;
Ok(map)
}
match request.json::<vanilla_manifest::MinecraftManifest>().await {
Ok(manifest) => Ok(manifest),
Err(e) => Err(e.into())
}
}
async fn get_minecraft_manifest(
version_url: String,
) -> Result<vanilla_manifest::MinecraftManifest, Box<dyn Error>> {
let request = http::create_client().get(version_url).send().await?;
fn get_handle(&self) -> &AppHandle {
&self.handle
}
match request.json::<vanilla_manifest::MinecraftManifest>().await {
Ok(manifest) => Ok(manifest),
Err(e) => Err(e.into()),
}
}
fn get_details(&self) -> &GameClientDetails {
&self.details
}
fn get_handle(&self) -> &AppHandle {
&self.handle
}
fn get_client(&self) -> &GameClientType {
&self.get_details().client_type
}
fn get_details(&self) -> &GameClientDetails {
&self.details
}
async fn setup(&self) -> Result<(), Box<dyn Error>> {
let libraries_dir = self.get_handle().path().app_config_dir()?.join("libraries");
let versions = Self::get_all_version_urls().await?;
let version = versions.get(&self.get_details().version).ok_or("Version not found")?.to_owned();
let manifest = Self::get_minecraft_manifest(version).await?;
fn get_client(&self) -> &GameClientType {
&self.get_details().client_type
}
let libraries = vanilla_impl::setup_libraries(&manifest.libraries, &libraries_dir).await?;
println!("{:#?}", libraries);
// vanilla_impl::setup_natives().await?;
// vanilla_impl::setup_assets().await?;
Ok(())
}
async fn setup(&self) -> Result<(), Box<dyn Error>> {
let libraries_dir = self.get_handle().path().app_config_dir()?.join("libraries");
let versions = Self::get_all_version_urls().await?;
let version = versions
.get(&self.get_details().version)
.ok_or("Version not found")?
.to_owned();
let manifest = Self::get_minecraft_manifest(version).await?;
async fn launch(&self) -> Result<(), Box<dyn Error>> {
println!("Launching Vanilla client");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
println!("Vanilla client launched");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
println!("Vanilla client exited");
stdout().flush().await?; // why...
Ok(())
}
let libraries = vanilla_impl::setup_libraries(&manifest.libraries, &libraries_dir).await?;
println!("{:#?}", libraries);
// vanilla_impl::setup_natives().await?;
// vanilla_impl::setup_assets().await?;
Ok(())
}
async fn launch(&self) -> Result<(), Box<dyn Error>> {
println!("Launching Vanilla client");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
println!("Vanilla client launched");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
println!("Vanilla client exited");
stdout().flush().await?; // why...
Ok(())
}
}
pub mod vanilla_impl {
use std::{error::Error, fs, path::PathBuf};
use std::{error::Error, fs, path::PathBuf};
use serde_json::{Map, Value};
use tauri::AppHandle;
use serde_json::{Map, Value};
use tauri::AppHandle;
use crate::utils::http::download_file;
use crate::utils::http::download_file;
use super::vanilla_manifest::{self, Library};
use super::vanilla_manifest::{self, Library};
pub async fn setup_libraries(libraries: &Vec<Library>, libraries_folder: &PathBuf) -> Result<Vec<String>, Box<dyn Error>> {
let mut natives_ret: Vec<&Library> = vec![];
let mut libraries_ret: Vec<String> = vec![];
pub async fn setup_libraries(
libraries: &Vec<Library>,
libraries_folder: &PathBuf,
) -> Result<Vec<String>, Box<dyn Error>> {
let mut natives_ret: Vec<&Library> = vec![];
let mut libraries_ret: Vec<String> = vec![];
for library in libraries {
// let library: &Map<String, Value> = library.as_object().unwrap();
// if let Some(_) = library.get("natives") {
// natives_ret.push(library);
// continue;
// }
// let artifact = library.get("downloads").ok_or("No downloads object")?
// .as_object().ok_or("Invalid downloads object")?
// .get("artifact").ok_or("No artifact object")?
// .as_object().ok_or("Invalid artifact object")?;
for library in libraries {
// let library: &Map<String, Value> = library.as_object().unwrap();
// let path = artifact.get("path").ok_or("No path object")?
// .as_str().ok_or("Invalid path object")?;
// let url = artifact.get("url").ok_or("No url object")?
// .as_str().ok_or("Invalid url object")?;
// if let Some(_) = library.get("natives") {
// natives_ret.push(library);
// continue;
// }
if let Some(_) = library.natives {
natives_ret.push(library);
continue;
}
// let artifact = library.get("downloads").ok_or("No downloads object")?
// .as_object().ok_or("Invalid downloads object")?
// .get("artifact").ok_or("No artifact object")?
// .as_object().ok_or("Invalid artifact object")?;
let artifact = library.downloads.artifact.clone().ok_or("No artifact object")?;
let path = artifact.path;
let url = artifact.url;
// TODO: Add checks for rules + platform
let dest = libraries_folder.join(path);
fs::create_dir_all(dest.parent().ok_or("Couldn't get library parent")?)?;
// let path = artifact.get("path").ok_or("No path object")?
// .as_str().ok_or("Invalid path object")?;
// let url = artifact.get("url").ok_or("No url object")?
// .as_str().ok_or("Invalid url object")?;
if !dest.exists() {
download_file(url.as_str(), &dest).await?;
}
if let Some(_) = library.natives {
natives_ret.push(library);
continue;
}
libraries_ret.push(dest.to_str().ok_or("Couldn't get library path")?.to_string());
}
let artifact = library
.downloads
.artifact
.clone()
.ok_or("No artifact object")?;
let path = artifact.path;
let url = artifact.url;
Ok(libraries_ret)
}
// TODO: Add checks for rules + platform
pub async fn setup_natives() -> Result<(), Box<dyn Error>> {
Ok(())
}
let dest = libraries_folder.join(path);
fs::create_dir_all(dest.parent().ok_or("Couldn't get library parent")?)?;
pub async fn setup_assets() -> Result<(), Box<dyn Error>> {
Ok(())
}
if !dest.exists() {
download_file(url.as_str(), &dest).await?;
}
libraries_ret.push(
dest.to_str()
.ok_or("Couldn't get library path")?
.to_string(),
);
}
Ok(libraries_ret)
}
pub async fn setup_natives() -> Result<(), Box<dyn Error>> {
Ok(())
}
pub async fn setup_assets() -> Result<(), Box<dyn Error>> {
Ok(())
}
}
// todo move to prisma
pub mod vanilla_manifest {
use serde::Deserialize;
use serde::Serialize;
use serde::Deserialize;
use serde::Serialize;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftManifest {
pub asset_index: AssetIndex,
pub assets: String,
pub compliance_level: i64,
pub downloads: Downloads,
pub id: String,
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub logging: Logging,
pub main_class: String,
pub minecraft_arguments: String,
pub minimum_launcher_version: i64,
pub release_time: String,
pub time: String,
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftManifest {
pub asset_index: AssetIndex,
pub assets: String,
pub compliance_level: i64,
pub downloads: Downloads,
pub id: String,
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub logging: Logging,
pub main_class: String,
pub minecraft_arguments: String,
pub minimum_launcher_version: i64,
pub release_time: String,
pub time: String,
#[serde(rename = "type")]
pub type_field: String,
}
#[serde(rename = "type")]
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
pub size: i64,
pub total_size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
pub size: i64,
pub total_size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Downloads {
pub client: Client,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Downloads {
pub client: Client,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Client {
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Client {
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaVersion {
pub component: String,
pub major_version: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaVersion {
pub component: String,
pub major_version: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Library {
pub downloads: Downloads2,
pub name: String,
#[serde(default)]
pub rules: Vec<Rule>,
pub extract: Option<Extract>,
pub natives: Option<Natives>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Library {
pub downloads: Downloads2,
pub name: String,
#[serde(default)]
pub rules: Vec<Rule>,
pub extract: Option<Extract>,
pub natives: Option<Natives>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Downloads2 {
pub artifact: Option<Artifact>,
pub classifiers: Option<Classifiers>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Downloads2 {
pub artifact: Option<Artifact>,
pub classifiers: Option<Classifiers>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub path: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub path: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Classifiers {
#[serde(rename = "natives-windows-32")]
pub natives_windows_32: Option<NativeLibrary>,
#[serde(rename = "natives-windows-64")]
pub natives_windows_64: Option<NativeLibrary>,
#[serde(rename = "natives-osx")]
pub natives_osx: Option<NativeLibrary>,
#[serde(rename = "natives-linux")]
pub natives_linux: Option<NativeLibrary>,
#[serde(rename = "natives-windows")]
pub natives_windows: Option<NativeLibrary>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Classifiers {
#[serde(rename = "natives-windows-32")]
pub natives_windows_32: Option<NativeLibrary>,
#[serde(rename = "natives-windows-64")]
pub natives_windows_64: Option<NativeLibrary>,
#[serde(rename = "natives-osx")]
pub natives_osx: Option<NativeLibrary>,
#[serde(rename = "natives-linux")]
pub natives_linux: Option<NativeLibrary>,
#[serde(rename = "natives-windows")]
pub natives_windows: Option<NativeLibrary>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NativeLibrary {
pub path: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NativeLibrary {
pub path: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub action: String,
pub os: Option<Os>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub action: String,
pub os: Option<Os>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Os {
pub name: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Os {
pub name: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Extract {
pub exclude: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Extract {
pub exclude: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Natives {
pub windows: String,
pub linux: Option<String>,
pub osx: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Natives {
pub windows: String,
pub linux: Option<String>,
pub osx: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Logging {
pub client: Client2,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Logging {
pub client: Client2,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Client2 {
pub argument: String,
pub file: File,
#[serde(rename = "type")]
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct File {
pub id: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Client2 {
pub argument: String,
pub file: File,
#[serde(rename = "type")]
pub type_field: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct File {
pub id: String,
pub sha1: String,
pub size: i64,
pub url: String,
}
}

View file

@ -11,72 +11,73 @@ mod clients;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum JavaVersion {
V8,
V16,
V17
V8,
V16,
V17,
}
impl ToString for JavaVersion {
fn to_string(&self) -> String {
match self {
JavaVersion::V8 => "8".to_string(),
JavaVersion::V16 => "16".to_string(),
JavaVersion::V17 => "17".to_string()
}
}
fn to_string(&self) -> String {
match self {
JavaVersion::V8 => "8".to_string(),
JavaVersion::V16 => "16".to_string(),
JavaVersion::V17 => "17".to_string(),
}
}
}
// Tauri plugin for the game module
pub struct GameManagerState {
pub selected_client: Option<Box<dyn client::GameClient>>,
pub selected_client: Option<Box<dyn client::GameClient>>,
}
unsafe impl Send for GameManagerState {}
unsafe impl Sync for GameManagerState {}
impl Default for GameManagerState {
fn default() -> Self {
GameManagerState {
selected_client: None
}
}
fn default() -> Self {
GameManagerState {
selected_client: None,
}
}
}
pub fn init() -> TauriPlugin<tauri::Wry> {
tauri::plugin::Builder::new("game")
.setup(|app, _| {
app.manage(Mutex::new(GameManagerState::default()));
Ok(())
})
.invoke_handler(tauri::generate_handler![
launch_game,
set_selected_client
])
.build()
tauri::plugin::Builder::new("game")
.setup(|app, _| {
app.manage(Mutex::new(GameManagerState::default()));
Ok(())
})
.invoke_handler(tauri::generate_handler![launch_game, set_selected_client])
.build()
}
#[tauri::command]
async fn launch_game(state: tauri::State<'_, Mutex<GameManagerState>>) -> Result<(), String> {
let state = state.lock().await;
let selected_client = match &state.selected_client {
Some(client) => client,
None => return Err("No client selected".into())
};
let state = state.lock().await;
let selected_client = match &state.selected_client {
Some(client) => client,
None => return Err("No client selected".into()),
};
if let Err(err) = selected_client.setup().await {
return Err(format!("An error has occurred during install: {}", err));
}
if let Err(err) = selected_client.setup().await {
return Err(format!("An error has occurred during install: {}", err));
}
match selected_client.launch().await {
Ok(_) => Ok(()),
Err(e) => Err(format!("An error has occurred: {}", e))
}
match selected_client.launch().await {
Ok(_) => Ok(()),
Err(e) => Err(format!("An error has occurred: {}", e)),
}
}
// TODO: Implement setting client by UUID, as currently this is creating a new client and setting that
#[tauri::command]
async fn set_selected_client(handle: tauri::AppHandle, state: tauri::State<'_, Mutex<GameManagerState>>, details: client::GameClientDetails) -> Result<(), String> {
let mut state = state.lock().await;
let client = get_game_client(handle, details);
state.selected_client = Some(client);
Ok(())
async fn set_selected_client(
handle: tauri::AppHandle,
state: tauri::State<'_, Mutex<GameManagerState>>,
details: client::GameClientDetails,
) -> Result<(), String> {
let mut state = state.lock().await;
let client = get_game_client(handle, details);
state.selected_client = Some(client);
Ok(())
}

View file

@ -1,58 +1,58 @@
use thiserror::Error;
pub mod auth;
pub mod constants;
pub mod game;
pub mod utils;
pub mod constants;
/// A standardized [`Error`] type that is used across the launcher
#[derive(Debug, Error)]
pub enum PolyError {
#[error("a tauri runtime error occured (this should not happen): {0}")]
/// Wrapper around [`tauri::Error`] to handle expected Tauri errors.
TauriError(#[from] tauri::Error),
#[error("failed to manage shell task: {0}")]
/// Wrapper around [`tauri_plugin_shell::Error`] to handle shell errors.
TauriShellError(#[from] tauri_plugin_shell::Error),
#[error("failed to manage a tokio semaphore: {0}")]
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
TokioError(#[from] tokio::sync::AcquireError),
#[error("failed to compress a file with flate: {0}")]
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
FlateError(#[from] flate2::CompressError),
// im actually the funniest person alive dont @ me
#[error("failed to decompress a file with flate: {0}")]
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
DeflateError(#[from] flate2::DecompressError),
#[error("failed to parse uuids: {0}")]
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
UUIDError(#[from] uuid::Error),
#[error("failed to manage serialization of json: {0}")]
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
SerdeError(#[from] serde_json::Error),
#[error("failed to manage writing and reading of files: {0}")]
/// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`)
IOError(#[from] std::io::Error),
#[error("failed to query the database: {0}")]
/// Wrapper around [`prisma_client_rust::QueryError`] to handle DB query errors.
DBQueryError(#[from] prisma_client_rust::QueryError),
#[error("failed to establish a connection to the database: {0}")]
/// Wrapper around [`prisma_client_rust::NewClientError`] to handle errors occuring while programmatically connecting to Prisma.
DBConnectionError(#[from] prisma_client_rust::NewClientError),
#[error("failed to establish a HTTP connection: {0}")]
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
HTTPError(#[from] tauri_plugin_http::reqwest::Error),
#[error("failed to download java: {0}")]
/// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading.
JavaError(#[from] game::client::JavaDownloadError),
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
#[error("failed to manage zip files: {0}")]
ZipError(#[from] zip::result::ZipError),
#[error("failed to authenticate: {0}")]
// Wrapper around [`auth::AuthenticationError`] to handle authentication errors.
AuthError(#[from] auth::AuthenticationError),
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
#[error("a tauri runtime error occured (this should not happen): {0}")]
/// Wrapper around [`tauri::Error`] to handle expected Tauri errors.
TauriError(#[from] tauri::Error),
#[error("failed to manage shell task: {0}")]
/// Wrapper around [`tauri_plugin_shell::Error`] to handle shell errors.
TauriShellError(#[from] tauri_plugin_shell::Error),
#[error("failed to manage a tokio semaphore: {0}")]
/// Wrapper around [`tokio::sync::AcquireError`] to handle sempahore errors.
TokioError(#[from] tokio::sync::AcquireError),
#[error("failed to compress a file with flate: {0}")]
/// Wrapper around [`flate2::CompressError`] to handle flate compression errors.
FlateError(#[from] flate2::CompressError),
// im actually the funniest person alive dont @ me
#[error("failed to decompress a file with flate: {0}")]
/// Wrapper around [`flate2::DecompressError`] to handle flate decompression errors.
DeflateError(#[from] flate2::DecompressError),
#[error("failed to parse uuids: {0}")]
/// Wrapper around [`uuid::Error`] to handle UUID parsing errors.
UUIDError(#[from] uuid::Error),
#[error("failed to manage serialization of json: {0}")]
/// Wrapper around [`serde_json::Error`] to handle Serde JSON parsing errors.
SerdeError(#[from] serde_json::Error),
#[error("failed to manage writing and reading of files: {0}")]
/// Wrapper around [`std::io::Error`] only to be used if a more defined error isn't available (e.g. when using `zip`)
IOError(#[from] std::io::Error),
#[error("failed to query the database: {0}")]
/// Wrapper around [`prisma_client_rust::QueryError`] to handle DB query errors.
DBQueryError(#[from] prisma_client_rust::QueryError),
#[error("failed to establish a connection to the database: {0}")]
/// Wrapper around [`prisma_client_rust::NewClientError`] to handle errors occuring while programmatically connecting to Prisma.
DBConnectionError(#[from] prisma_client_rust::NewClientError),
#[error("failed to establish a HTTP connection: {0}")]
/// Wrapper around [`reqwest::Error`] to handle HTTP errors.
HTTPError(#[from] tauri_plugin_http::reqwest::Error),
#[error("failed to download java: {0}")]
/// Wrapper around [`game::client::JavaDownloadError`] to handle Java executable downloading.
JavaError(#[from] game::client::JavaDownloadError),
/// Wrapper around [`zip::result::ZipError`] to handle zip errors.
#[error("failed to manage zip files: {0}")]
ZipError(#[from] zip::result::ZipError),
#[error("failed to authenticate: {0}")]
// Wrapper around [`auth::AuthenticationError`] to handle authentication errors.
AuthError(#[from] auth::AuthenticationError),
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
}
/// Alias for a [`Result`] with the error type [`PolyError`].

View file

@ -7,16 +7,16 @@ use polyfrost_launcher::{auth, game};
use tauri::{menu::Menu, App};
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(auth::init())
.plugin(game::init())
.menu(|handle| Menu::new(handle))
.setup(setup)
.run(tauri::generate_context!())
.expect("error while running tauri application");
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(auth::init())
.plugin(game::init())
.menu(|handle| Menu::new(handle))
.setup(setup)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn setup(_: &mut App) -> Result<(), Box<dyn Error>> {
Ok(())
Ok(())
}

View file

@ -1,33 +1,41 @@
use std::{fs::File, path::Path};
use anyhow::anyhow;
use std::{fs::File, path::Path};
use crate::{PolyError, PolyResult};
pub fn extract_archive(archive: &Path, dest: &Path) -> PolyResult<()> {
let ext = match archive.extension() {
Some(ext) => ext.to_str().unwrap(),
None => return Err(PolyError::AnyhowError(anyhow!("unsupported operating system")))
};
let ext = match archive.extension() {
Some(ext) => ext.to_str().unwrap(),
None => {
return Err(PolyError::AnyhowError(anyhow!(
"unsupported operating system"
)))
}
};
match ext {
"zip" => extract_zip(archive, dest),
"gz" => extract_tar_gz(archive, dest),
_ => Err(PolyError::AnyhowError(anyhow!("unsupported file extension {:?}", ext)))
}
match ext {
"zip" => extract_zip(archive, dest),
"gz" => extract_tar_gz(archive, dest),
_ => Err(PolyError::AnyhowError(anyhow!(
"unsupported file extension {:?}",
ext
))),
}
}
pub fn extract_zip(archive: &Path, dest: &Path) -> PolyResult<()> {
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
let mut archive = zip::ZipArchive::new(file)
.map_err(|err| PolyError::ZipError(err));
archive?.extract(dest).map_err(|err| PolyError::ZipError(err));
Ok(())
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
let mut archive = zip::ZipArchive::new(file).map_err(|err| PolyError::ZipError(err));
archive?
.extract(dest)
.map_err(|err| PolyError::ZipError(err));
Ok(())
}
pub fn extract_tar_gz(archive: &Path, dest: &Path) -> PolyResult<()> {
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
let tar_gz = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(tar_gz);
archive.unpack(dest).map_err(|err| PolyError::IOError(err));
Ok(())
let file = File::open(archive).map_err(|err| PolyError::IOError(err))?;
let tar_gz = flate2::read::GzDecoder::new(file);
let mut archive = tar::Archive::new(tar_gz);
archive.unpack(dest).map_err(|err| PolyError::IOError(err));
Ok(())
}

View file

@ -1,20 +1,34 @@
use std::{fs::File, io::{self, Cursor}, path::Path};
use std::{
fs::File,
io::{self, Cursor},
path::Path,
};
use tauri_plugin_http::reqwest::{Client, ClientBuilder};
use crate::{constants::USER_AGENT, PolyError, PolyResult};
pub fn create_client() -> PolyResult<Client> {
ClientBuilder::new()
.user_agent(USER_AGENT)
.build().map_err(|err| PolyError::HTTPError(err))
ClientBuilder::new()
.user_agent(USER_AGENT)
.build()
.map_err(|err| PolyError::HTTPError(err))
}
// can we not make mapping the errors automatic, isnt that the whole point
pub async fn download_file(url: &str, path: &Path) -> PolyResult<()> {
let response = create_client()?.get(url).send().await.map_err(|err| PolyError::HTTPError(err))?;
let mut file = File::create(path).map_err(|err| PolyError::IOError(err))?;
let mut content = Cursor::new(response.bytes().await.map_err(|err| PolyError::HTTPError(err))?);
io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err));
Ok(())
let response = create_client()?
.get(url)
.send()
.await
.map_err(|err| PolyError::HTTPError(err))?;
let mut file = File::create(path).map_err(|err| PolyError::IOError(err))?;
let mut content = Cursor::new(
response
.bytes()
.await
.map_err(|err| PolyError::HTTPError(err))?,
);
io::copy(&mut content, &mut file).map_err(|err| PolyError::IOError(err));
Ok(())
}

View file

@ -1,2 +1,2 @@
pub mod file;
pub mod http;
pub mod file;