Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/images/icons/external/HMCL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/icons/external/PCL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/icons/external/SCL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions src-tauri/src/account/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::account::helpers::authlib_injector::info::{
};
use crate::account::helpers::authlib_injector::jar::check_authlib_jar;
use crate::account::helpers::authlib_injector::{self};
use crate::account::helpers::import::hmcl::retrieve_hmcl_account_info;
use crate::account::helpers::import::ImportLauncherType;
use crate::account::helpers::{microsoft, misc, offline};
use crate::account::models::{
AccountError, AccountInfo, AuthServer, DeviceAuthResponseInfo, Player, PlayerInfo, PlayerType,
Expand Down Expand Up @@ -619,3 +621,96 @@ pub fn delete_auth_server(app: AppHandle, url: String) -> SJMCLResult<()> {
config_state.save()?;
Ok(())
}

// Stage 1 of importing accounts (players and auth servers) from other launchers
#[tauri::command]
pub async fn retrieve_other_launcher_account_info(
app: AppHandle,
launcher_type: ImportLauncherType,
) -> SJMCLResult<(Vec<Player>, Vec<AuthServer>)> {
let (mut player_infos, urls) = match launcher_type {
ImportLauncherType::HMCL => retrieve_hmcl_account_info(&app).await?,
_ => return Ok((vec![], vec![])),
};

// remove trailing slashes for deduplication
let mut url_set = std::collections::HashSet::<String>::new();
for u in urls {
url_set.insert(u.as_str().trim_end_matches('/').to_string());
}
for p in &mut player_infos {
if let Some(url) = p.auth_server_url.as_mut() {
*url = url.trim_end_matches('/').to_string();
}
}

// fetch auth servers
let mut auth_server_infos = Vec::new();
for url in url_set {
auth_server_infos.push(fetch_auth_server_info(&app, url).await?);
}

Ok((
player_infos
.into_iter()
.map(|p| Player::from_player_info(p, Some(&auth_server_infos)))
.collect(),
auth_server_infos
.into_iter()
.map(AuthServer::from)
.collect(),
))
}

// Stage 2 of importing accounts from other launchers
#[tauri::command]
pub async fn import_external_account_info(
app: AppHandle,
players: Vec<Player>,
auth_servers: Vec<AuthServer>,
) -> SJMCLResult<()> {
// fetch auth servers
let fetch_tasks = auth_servers.into_iter().map(|server| {
let app = app.clone();
async move { fetch_auth_server_info(&app, server.auth_url).await }
});

let fetched = futures::future::join_all(fetch_tasks).await;
let mut fetched_infos = Vec::with_capacity(fetched.len());
for r in fetched {
fetched_infos.push(r?);
}

let account_binding = app.state::<Mutex<AccountInfo>>();
let mut account_state = account_binding.lock()?;

// servers: same url overwritten
for server_info in fetched_infos {
if let Some(existing) = account_state
.auth_servers
.iter_mut()
.find(|s| s.auth_url == server_info.auth_url)
{
*existing = server_info;
} else {
account_state.auth_servers.push(server_info);
}
}

// players: same id overwritten
for player in players {
let player_info: PlayerInfo = player.into();
if let Some(existing) = account_state
.players
.iter_mut()
.find(|p| p.id == player_info.id)
{
*existing = player_info;
} else {
account_state.players.push(player_info);
}
}

account_state.save()?;
Ok(())
}
32 changes: 17 additions & 15 deletions src-tauri/src/account/helpers/authlib_injector/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,24 @@ pub async fn parse_profile(
.map(|b| b as char)
.collect::<String>();

let texture_info_value: TextureInfo =
serde_json::from_str(&texture_info).map_err(|_| AccountError::ParseError)?;
if !texture_info.is_empty() {
let texture_info_value: TextureInfo =
serde_json::from_str(&texture_info).map_err(|_| AccountError::ParseError)?;

for texture_type in TextureType::iter() {
if let Some(skin) = texture_info_value.textures.get(&texture_type.to_string()) {
textures.push(Texture {
image: fetch_image(app, skin.url.clone()).await?,
texture_type,
model: skin
.metadata
.as_ref()
.and_then(|metadata| metadata.get("model").cloned())
.map(|model_str| SkinModel::from_str(&model_str).unwrap_or(SkinModel::Default))
.unwrap_or_default(),
preset: None,
});
for texture_type in TextureType::iter() {
if let Some(skin) = texture_info_value.textures.get(&texture_type.to_string()) {
textures.push(Texture {
image: fetch_image(app, skin.url.clone()).await?,
texture_type,
model: skin
.metadata
.as_ref()
.and_then(|metadata| metadata.get("model").cloned())
.map(|model_str| SkinModel::from_str(&model_str).unwrap_or(SkinModel::Default))
.unwrap_or_default(),
preset: None,
});
}
}
}
}
Expand Down
21 changes: 11 additions & 10 deletions src-tauri/src/account/helpers/authlib_injector/models.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use std::collections::HashMap;

structstruck::strike! {
#[strikethrough[derive(serde::Deserialize, serde::Serialize)]]
pub struct MinecraftProfile {
pub id: String,
pub name: String,
pub properties: Option<Vec<pub struct {
pub name: String,
pub value: String,
}>>
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct MinecraftProfileProperty {
pub name: String,
pub value: String,
}

#[derive(serde::Deserialize, serde::Serialize)]
pub struct MinecraftProfile {
pub id: String,
pub name: String,
pub properties: Option<Vec<MinecraftProfileProperty>>,
}

structstruck::strike! {
Expand Down
214 changes: 214 additions & 0 deletions src-tauri/src/account/helpers/import/hmcl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use crate::account::helpers::authlib_injector::common::parse_profile;
use crate::account::helpers::authlib_injector::models::{
MinecraftProfile, MinecraftProfileProperty,
};
use crate::account::helpers::microsoft::oauth::fetch_minecraft_profile;
use crate::account::helpers::misc::fetch_image;
use crate::account::helpers::offline::load_preset_skin;
use crate::account::models::{
AccountError, PlayerInfo, PlayerType, PresetRole, SkinModel, Texture, TextureType,
};
use crate::error::SJMCLResult;
use serde::Deserialize;
use std::collections::HashSet;
use std::fs;
use std::str::FromStr;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use url::Url;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize)]
pub struct HmclOfflineAccount {
pub uuid: String,
pub username: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HmclMicrosoftAccount {
pub uuid: String,
pub display_name: String,
pub token_type: String,
pub access_token: String,
pub refresh_token: String,
pub not_after: i64,
pub userid: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct HmclProfileProperties {
pub textures: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HmclThirdPartyAccount {
#[serde(rename = "serverBaseURL")]
pub server_base_url: String,
pub client_token: String,
pub display_name: String,
pub access_token: String,
pub profile_properties: HmclProfileProperties,
pub uuid: String,
pub username: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum HmclAccountEntry {
#[serde(rename = "offline")]
Offline(HmclOfflineAccount),
#[serde(rename = "microsoft")]
Microsoft(HmclMicrosoftAccount),
#[serde(rename = "authlibInjector")]
ThirdParty(HmclThirdPartyAccount),
}

async fn offline_to_player(app: &AppHandle, acc: &HmclOfflineAccount) -> SJMCLResult<PlayerInfo> {
let uuid = uuid::Uuid::parse_str(&acc.uuid).map_err(|_| AccountError::ParseError)?;
let textures = load_preset_skin(app, PresetRole::Steve)?;
Ok(
PlayerInfo {
id: "".to_string(),
uuid,
name: acc.username.clone(),
player_type: PlayerType::Offline,
auth_account: None,
auth_server_url: None,
access_token: None,
refresh_token: None,
textures,
}
.with_generated_id(),
)
}

async fn microsoft_to_player(
app: &AppHandle,
acc: &HmclMicrosoftAccount,
) -> SJMCLResult<PlayerInfo> {
let profile = fetch_minecraft_profile(app, acc.access_token.clone()).await?;

let mut textures = vec![];
if let Some(skins) = &profile.skins {
for skin in skins {
if skin.state == "ACTIVE" {
textures.push(Texture {
texture_type: TextureType::Skin,
image: fetch_image(app, skin.url.clone()).await?,
model: skin.variant.clone().unwrap_or_default(),
preset: None,
});
}
}
}
if let Some(capes) = &profile.capes {
for cape in capes {
if cape.state == "ACTIVE" {
textures.push(Texture {
texture_type: TextureType::Cape,
image: fetch_image(app, cape.url.clone()).await?,
model: SkinModel::Default,
preset: None,
});
}
}
}

if textures.is_empty() {
// this player didn't have a texture, use preset Steve skin instead
textures = load_preset_skin(app, PresetRole::Steve)?;
}

Ok(
PlayerInfo {
id: "".to_string(),
uuid: Uuid::from_str(&profile.id).map_err(|_| AccountError::ParseError)?,
name: profile.name.clone(),
player_type: PlayerType::Microsoft,
auth_account: Some(profile.name.clone()),
access_token: Some(acc.access_token.clone()),
refresh_token: Some(acc.refresh_token.clone()),
textures,
auth_server_url: None,
}
.with_generated_id(),
)
}

async fn thirdparty_to_player(
app: &AppHandle,
acc: &HmclThirdPartyAccount,
) -> SJMCLResult<PlayerInfo> {
let profile = MinecraftProfile {
id: acc.uuid.clone(),
name: acc.display_name.clone(),
properties: Some(vec![MinecraftProfileProperty {
name: "textures".to_string(),
value: acc.profile_properties.textures.clone().unwrap_or_default(),
}]),
};
let p = parse_profile(
app,
&profile,
Some(acc.access_token.clone()),
None,
Some(acc.server_base_url.clone()),
Some(acc.username.clone()),
)
.await?;
Ok(p)
}

pub async fn retrieve_hmcl_account_info(
app: &AppHandle,
) -> SJMCLResult<(Vec<PlayerInfo>, Vec<Url>)> {
let hmcl_json_path = if cfg!(target_os = "linux") {
app
.path()
.resolve("", BaseDirectory::Home)?
.join(".hmcl")
.join("accounts.json")
} else {
let app_data = app.path().resolve("", BaseDirectory::AppData)?;
let base = app_data
.parent()
.ok_or(AccountError::NotFound)?
.to_path_buf();
if cfg!(target_os = "macos") {
base.join("hmcl").join("accounts.json")
} else {
base.join(".hmcl").join("accounts.json")
}
};

if !hmcl_json_path.is_file() {
return Ok((vec![], vec![]));
}

let hmcl_json = fs::read_to_string(&hmcl_json_path).map_err(|_| AccountError::NotFound)?;

let hmcl_entries: Vec<HmclAccountEntry> =
serde_json::from_str(&hmcl_json).map_err(|_| AccountError::Invalid)?;
let mut player_infos: Vec<PlayerInfo> = Vec::new();
let mut url_set: HashSet<Url> = HashSet::new();

for e in &hmcl_entries {
match e {
HmclAccountEntry::Offline(acc) => {
player_infos.push(offline_to_player(app, acc).await?);
}
HmclAccountEntry::Microsoft(acc) => {
player_infos.push(microsoft_to_player(app, acc).await?);
}
HmclAccountEntry::ThirdParty(acc) => {
if let Ok(url) = Url::parse(&acc.server_base_url) {
url_set.insert(url);
}
player_infos.push(thirdparty_to_player(app, acc).await?);
}
}
}

Ok((player_infos, url_set.into_iter().collect()))
}
Loading
Loading