feat: mojang account configuration (skins / capes)
This commit is contained in:
parent
96c0ca4d5a
commit
3b8afb3289
3 changed files with 208 additions and 19 deletions
|
|
@ -193,11 +193,33 @@ pub trait TauriLauncherApi {
|
||||||
// endregion: modpack
|
// endregion: modpack
|
||||||
|
|
||||||
// MARK: API: minecraft
|
// MARK: API: minecraft
|
||||||
|
// region: minecraft
|
||||||
#[taurpc(alias = "fetchMinecraftProfile")]
|
#[taurpc(alias = "fetchMinecraftProfile")]
|
||||||
async fn fetch_player_profile(
|
async fn fetch_player_profile(
|
||||||
uuid: String,
|
uuid: String,
|
||||||
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile>;
|
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile>;
|
||||||
|
|
||||||
|
#[taurpc(alias = "fetchLoggedInProfile")]
|
||||||
|
async fn fetch_logged_in_profile(
|
||||||
|
access_token: String,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangFullPlayerProfile>;
|
||||||
|
|
||||||
|
#[taurpc(alias = "uploadSkinBytes")]
|
||||||
|
async fn upload_skin_bytes(
|
||||||
|
access_token: String,
|
||||||
|
skin_data: Vec<u8>,
|
||||||
|
image_format: String,
|
||||||
|
skin_variant: crate::utils::minecraft::SkinVariant,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangSkin>;
|
||||||
|
|
||||||
|
#[taurpc(alias = "changeSkin")]
|
||||||
|
async fn change_skin(
|
||||||
|
access_token: String,
|
||||||
|
skin_url: String,
|
||||||
|
skin_variant: crate::utils::minecraft::SkinVariant,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangSkin>;
|
||||||
|
// endregion: minecraft
|
||||||
|
|
||||||
// MARK: API: Other
|
// MARK: API: Other
|
||||||
async fn open(input: String) -> LauncherResult<()>;
|
async fn open(input: String) -> LauncherResult<()>;
|
||||||
}
|
}
|
||||||
|
|
@ -674,6 +696,38 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
||||||
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile> {
|
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile> {
|
||||||
crate::utils::minecraft::fetch_player_profile(&uuid).await
|
crate::utils::minecraft::fetch_player_profile(&uuid).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn fetch_logged_in_profile(
|
||||||
|
self,
|
||||||
|
access_token: String,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangFullPlayerProfile> {
|
||||||
|
crate::utils::minecraft::fetch_logged_in_profile(&access_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_skin_bytes(
|
||||||
|
self,
|
||||||
|
access_token: String,
|
||||||
|
skin_data: Vec<u8>,
|
||||||
|
image_format: String,
|
||||||
|
skin_variant: crate::utils::minecraft::SkinVariant,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangSkin> {
|
||||||
|
crate::utils::minecraft::upload_skin_bytes(
|
||||||
|
&access_token,
|
||||||
|
skin_data,
|
||||||
|
&image_format,
|
||||||
|
skin_variant,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn change_skin(
|
||||||
|
self,
|
||||||
|
access_token: String,
|
||||||
|
skin_url: String,
|
||||||
|
skin_variant: crate::utils::minecraft::SkinVariant,
|
||||||
|
) -> LauncherResult<crate::utils::minecraft::MojangSkin> {
|
||||||
|
crate::utils::minecraft::change_skin(&access_token, &skin_url, skin_variant).await
|
||||||
|
}
|
||||||
// endregion: minecraft
|
// endregion: minecraft
|
||||||
|
|
||||||
// MARK: Impl: Other
|
// MARK: Impl: Other
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::path::Path;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use reqwest::Method;
|
use reqwest::{Method, RequestBuilder};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
|
@ -50,34 +50,32 @@ pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
});
|
});
|
||||||
|
|
||||||
pub async fn request(method: Method, url: &str) -> LauncherResult<reqwest::Response> {
|
pub async fn request(method: Method, url: &str) -> LauncherResult<reqwest::Response> {
|
||||||
request_builder(
|
let req = build_request(method, url).build()?;
|
||||||
method,
|
send_request(req).await
|
||||||
url,
|
|
||||||
None::<fn(reqwest::RequestBuilder) -> reqwest::Result<_>>,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_builder(
|
pub fn build_request(method: Method, url: &str) -> RequestBuilder {
|
||||||
method: Method,
|
let client = &REQWEST_CLIENT;
|
||||||
url: &str,
|
|
||||||
builder: Option<impl Fn(reqwest::RequestBuilder) -> reqwest::Result<reqwest::Request>>,
|
client.request(method.clone(), url)
|
||||||
) -> LauncherResult<reqwest::Response> {
|
}
|
||||||
|
|
||||||
|
pub async fn send_request(request: reqwest::Request) -> LauncherResult<reqwest::Response> {
|
||||||
let semaphore = SemaphoreStore::fetch().await;
|
let semaphore = SemaphoreStore::fetch().await;
|
||||||
let permit = semaphore.acquire().await;
|
let permit = semaphore.acquire().await;
|
||||||
|
|
||||||
let client = &REQWEST_CLIENT;
|
let client = &REQWEST_CLIENT;
|
||||||
|
|
||||||
let mut attempt = 0;
|
let mut attempt = 0;
|
||||||
|
|
||||||
let res = loop {
|
let res = loop {
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
|
|
||||||
let req = client.request(method.clone(), url);
|
let req = request.try_clone().ok_or_else(|| {
|
||||||
let req = match builder {
|
LauncherError::from(anyhow::anyhow!(
|
||||||
Some(ref builder) => builder(req),
|
"failed to clone request for retry attempt {attempt}"
|
||||||
None => req.build(),
|
))
|
||||||
}
|
})?;
|
||||||
.map_err(LauncherError::from)?;
|
|
||||||
|
|
||||||
match client.execute(req).await {
|
match client.execute(req).await {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use reqwest::Method;
|
use reqwest::{Method, multipart};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::error::LauncherResult;
|
use crate::error::LauncherResult;
|
||||||
use crate::utils::http;
|
use crate::utils::http;
|
||||||
|
|
@ -14,6 +17,50 @@ pub struct MojangPlayerProfile {
|
||||||
pub cape_url: Option<String>,
|
pub cape_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[onelauncher_macro::specta]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SkinVariant {
|
||||||
|
Classic,
|
||||||
|
Slim,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SkinVariant {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SkinVariant::Classic => write!(f, "classic"),
|
||||||
|
SkinVariant::Slim => write!(f, "slim"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[onelauncher_macro::specta]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||||
|
pub struct MojangFullPlayerProfile {
|
||||||
|
pub uuid: String,
|
||||||
|
pub username: String,
|
||||||
|
pub skins: Vec<MojangSkin>,
|
||||||
|
pub capes: Vec<MojangCape>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[onelauncher_macro::specta]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||||
|
pub struct MojangSkin {
|
||||||
|
pub id: String,
|
||||||
|
pub state: String,
|
||||||
|
pub url: String,
|
||||||
|
pub variant: SkinVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[onelauncher_macro::specta]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||||
|
pub struct MojangCape {
|
||||||
|
pub id: String,
|
||||||
|
pub state: String,
|
||||||
|
pub url: String,
|
||||||
|
pub alias: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProfile> {
|
pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProfile> {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct Properties {
|
struct Properties {
|
||||||
|
|
@ -98,3 +145,93 @@ pub async fn fetch_player_profile(uuid: &str) -> LauncherResult<MojangPlayerProf
|
||||||
cape_url: decoded.cape.as_ref().map(|c| c.url.clone()),
|
cape_url: decoded.cape.as_ref().map(|c| c.url.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_logged_in_profile(
|
||||||
|
access_token: &str,
|
||||||
|
) -> LauncherResult<MojangFullPlayerProfile> {
|
||||||
|
let mut headers: HashMap<&str, &str> = HashMap::with_capacity(1);
|
||||||
|
|
||||||
|
let bearer = &format!("Bearer {access_token}");
|
||||||
|
headers.insert("Authorization", bearer);
|
||||||
|
|
||||||
|
http::fetch_json_advanced::<MojangFullPlayerProfile>(
|
||||||
|
Method::GET,
|
||||||
|
"https://api.minecraftservices.com/minecraft/profile",
|
||||||
|
None,
|
||||||
|
Some(headers),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_skin_bytes(
|
||||||
|
access_token: &str,
|
||||||
|
skin_data: Vec<u8>,
|
||||||
|
image_format: &str,
|
||||||
|
skin_variant: SkinVariant,
|
||||||
|
) -> LauncherResult<MojangSkin> {
|
||||||
|
let bytes_part =
|
||||||
|
multipart::Part::bytes(skin_data).mime_str(&format!("image/{}", image_format))?;
|
||||||
|
|
||||||
|
let form = multipart::Form::new()
|
||||||
|
.text("variant", skin_variant.to_string())
|
||||||
|
.part("data", bytes_part);
|
||||||
|
|
||||||
|
let req = http::build_request(
|
||||||
|
Method::POST,
|
||||||
|
"https://api.minecraftservices.com/minecraft/profile/skins",
|
||||||
|
)
|
||||||
|
.header("Authorization", &format!("Bearer {access_token}"))
|
||||||
|
.header("Content-Type", "multipart/form-data")
|
||||||
|
.multipart(form)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let res = http::send_request(req).await?;
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
let status = res.status();
|
||||||
|
let text = res.text().await.unwrap_or_default();
|
||||||
|
return Err(
|
||||||
|
anyhow::anyhow!("failed to upload skin: HTTP {}: {}", status.as_u16(), text).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let full_profile = res.json::<MojangFullPlayerProfile>().await?;
|
||||||
|
dbg!(&full_profile);
|
||||||
|
full_profile
|
||||||
|
.skins
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no skins found in response").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn change_skin(
|
||||||
|
access_token: &str,
|
||||||
|
skin_url: &str,
|
||||||
|
skin_variant: SkinVariant,
|
||||||
|
) -> LauncherResult<MojangSkin> {
|
||||||
|
let mut headers: HashMap<&str, &str> = HashMap::with_capacity(1);
|
||||||
|
|
||||||
|
let bearer = &format!("Bearer {access_token}");
|
||||||
|
headers.insert("Authorization", bearer);
|
||||||
|
|
||||||
|
let profile = http::fetch_json_advanced::<MojangFullPlayerProfile>(
|
||||||
|
Method::POST,
|
||||||
|
"https://api.minecraftservices.com/minecraft/profile/skins",
|
||||||
|
Some(json!({
|
||||||
|
"url": skin_url,
|
||||||
|
"variant": skin_variant.to_string(),
|
||||||
|
})),
|
||||||
|
Some(headers),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
profile
|
||||||
|
.skins
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("no skins found in response").into())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue