Files
Starlight_Lancher/packages/app-lib/src/state/minecraft_skins/mojang_api.rs

170 lines
5.4 KiB
Rust

use std::{error::Error, sync::Arc, time::Instant};
use bytes::Bytes;
use futures::TryStream;
use reqwest::{Body, multipart::Part};
use serde_json::json;
use uuid::Uuid;
use super::MinecraftSkinVariant;
use crate::{
ErrorKind,
data::Credentials,
state::{
MINECRAFT_SERVICES_USER_AGENT, MinecraftProfile, PROFILE_CACHE,
ProfileCacheEntry,
},
util::fetch::INSECURE_REQWEST_CLIENT,
util::mojang::{mojang_service_url, should_use_mojang_mirror},
};
/// Provides operations for interacting with capes on a Minecraft player profile.
pub struct MinecraftCapeOperation;
impl MinecraftCapeOperation {
pub async fn equip(
credentials: &Credentials,
cape_id: Uuid,
) -> crate::Result<()> {
let url = mojang_service_url(
"https://api.minecraftservices.com/minecraft/profile/capes/active",
should_use_mojang_mirror(),
);
update_profile_cache_from_response(
INSECURE_REQWEST_CLIENT
.put(url.as_ref())
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
.bearer_auth(&credentials.access_token)
.json(&json!({
"capeId": cape_id.hyphenated(),
}))
.send()
.await
.and_then(|response| response.error_for_status())?,
)
.await;
Ok(())
}
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
let url = mojang_service_url(
"https://api.minecraftservices.com/minecraft/profile/capes/active",
should_use_mojang_mirror(),
);
update_profile_cache_from_response(
INSECURE_REQWEST_CLIENT
.delete(url.as_ref())
.header("Accept", "application/json")
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
.bearer_auth(&credentials.access_token)
.send()
.await
.and_then(|response| response.error_for_status())?,
)
.await;
Ok(())
}
}
/// Provides operations for interacting with skins on a Minecraft player profile.
pub struct MinecraftSkinOperation;
impl MinecraftSkinOperation {
pub async fn equip<TextureStream>(
credentials: &Credentials,
texture: TextureStream,
variant: MinecraftSkinVariant,
) -> crate::Result<Option<Arc<MinecraftProfile>>>
where
TextureStream: TryStream + Send + 'static,
TextureStream::Error: Into<Box<dyn Error + Send + Sync>>,
Bytes: From<TextureStream::Ok>,
{
let form = reqwest::multipart::Form::new()
.text(
"variant",
match variant {
MinecraftSkinVariant::Slim => "slim",
MinecraftSkinVariant::Classic => "classic",
_ => {
return Err(ErrorKind::OtherError(
"Cannot equip skin of unknown model variant".into(),
)
.into());
}
},
)
.part(
"file",
Part::stream(Body::wrap_stream(texture))
.mime_str("image/png")?
.file_name("skin.png"),
);
let url = mojang_service_url(
"https://api.minecraftservices.com/minecraft/profile/skins",
should_use_mojang_mirror(),
);
let profile = update_profile_cache_from_response(
INSECURE_REQWEST_CLIENT
.post(url.as_ref())
.header("Accept", "application/json")
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
.bearer_auth(&credentials.access_token)
.multipart(form)
.send()
.await
.and_then(|response| response.error_for_status())?,
)
.await;
Ok(profile)
}
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
let url = mojang_service_url(
"https://api.minecraftservices.com/minecraft/profile/skins/active",
should_use_mojang_mirror(),
);
update_profile_cache_from_response(
INSECURE_REQWEST_CLIENT
.delete(url.as_ref())
.header("Accept", "application/json")
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
.bearer_auth(&credentials.access_token)
.send()
.await
.and_then(|response| response.error_for_status())?,
)
.await;
Ok(())
}
}
async fn update_profile_cache_from_response(
response: reqwest::Response,
) -> Option<Arc<MinecraftProfile>> {
let Some(mut profile) = response.json::<MinecraftProfile>().await.ok()
else {
tracing::warn!(
"Failed to parse player profile from skin or cape operation response, not updating profile cache"
);
return None;
};
profile.fetch_time = Some(Instant::now());
let profile = Arc::new(profile);
PROFILE_CACHE
.lock()
.await
.insert(profile.id, ProfileCacheEntry::Hit(Arc::clone(&profile)));
Some(profile)
}