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
|
||||
|
||||
// MARK: API: minecraft
|
||||
// region: minecraft
|
||||
#[taurpc(alias = "fetchMinecraftProfile")]
|
||||
async fn fetch_player_profile(
|
||||
uuid: String,
|
||||
) -> 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
|
||||
async fn open(input: String) -> LauncherResult<()>;
|
||||
}
|
||||
|
|
@ -674,6 +696,38 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile> {
|
||||
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
|
||||
|
||||
// MARK: Impl: Other
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::path::Path;
|
|||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use bytes::Bytes;
|
||||
use reqwest::Method;
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::sync::Semaphore;
|
||||
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> {
|
||||
request_builder(
|
||||
method,
|
||||
url,
|
||||
None::<fn(reqwest::RequestBuilder) -> reqwest::Result<_>>,
|
||||
)
|
||||
.await
|
||||
let req = build_request(method, url).build()?;
|
||||
send_request(req).await
|
||||
}
|
||||
|
||||
pub async fn request_builder(
|
||||
method: Method,
|
||||
url: &str,
|
||||
builder: Option<impl Fn(reqwest::RequestBuilder) -> reqwest::Result<reqwest::Request>>,
|
||||
) -> LauncherResult<reqwest::Response> {
|
||||
pub fn build_request(method: Method, url: &str) -> RequestBuilder {
|
||||
let client = &REQWEST_CLIENT;
|
||||
|
||||
client.request(method.clone(), url)
|
||||
}
|
||||
|
||||
pub async fn send_request(request: reqwest::Request) -> LauncherResult<reqwest::Response> {
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
let permit = semaphore.acquire().await;
|
||||
|
||||
let client = &REQWEST_CLIENT;
|
||||
|
||||
let mut attempt = 0;
|
||||
|
||||
let res = loop {
|
||||
attempt += 1;
|
||||
|
||||
let req = client.request(method.clone(), url);
|
||||
let req = match builder {
|
||||
Some(ref builder) => builder(req),
|
||||
None => req.build(),
|
||||
}
|
||||
.map_err(LauncherError::from)?;
|
||||
let req = request.try_clone().ok_or_else(|| {
|
||||
LauncherError::from(anyhow::anyhow!(
|
||||
"failed to clone request for retry attempt {attempt}"
|
||||
))
|
||||
})?;
|
||||
|
||||
match client.execute(req).await {
|
||||
Err(err) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use base64::Engine;
|
||||
use reqwest::Method;
|
||||
use reqwest::{Method, multipart};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::error::LauncherResult;
|
||||
use crate::utils::http;
|
||||
|
|
@ -14,6 +17,50 @@ pub struct MojangPlayerProfile {
|
|||
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> {
|
||||
#[derive(serde::Deserialize)]
|
||||
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()),
|
||||
})
|
||||
}
|
||||
|
||||
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