You've already forked AstralRinth
fix: skins QA problems + flow change (#6216)
* fix: skins backend bugs + apply flow * fix: caching structure * feat: collapse already duplicated skins + fix moj api spam * fix: doc * fix: flatten migrations * feat: remove default cape/cape override concept * fix: fmt + lint * feat: remove SelectCapeModal for inline cape list * feat: qa * feat: virtualisation of skins sections + fix texture/model cache * fix: lint * fix: virt bugs + renderer fixes * fix: qa bugs * fix: doc * fix: re-add click impulse anim from prototypes + re-add interact anim length cap * fix: regressions * devex: split up SkinPreviewrenderer * fix: lint * fix: introduce dynamic mode in virtual-scroll.ts * feat: qa * fix: nametag bug + remove minecon skin pack suffix * feat: pain (literally) * feat: user agent on moj reqs * feat: impl per account flush queue for operations * fix: breadcrumb * chore: i18n pass * fix: lint + prep + check * fix: misalignments
This commit is contained in:
@@ -48,11 +48,6 @@ pub(crate) async fn connect(
|
||||
async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM default_minecraft_capes WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)"
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)"
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ use serde_json::json;
|
||||
use sha2::Digest;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::future::Future;
|
||||
use std::hash::{BuildHasherDefault, DefaultHasher};
|
||||
use std::io;
|
||||
@@ -217,6 +216,34 @@ pub(super) static PROFILE_CACHE: Mutex<
|
||||
HashMap<Uuid, ProfileCacheEntry, BuildHasherDefault<DefaultHasher>>,
|
||||
> = Mutex::const_new(HashMap::with_hasher(BuildHasherDefault::new()));
|
||||
|
||||
const ONLINE_PROFILE_CACHE_MAX_AGE: std::time::Duration =
|
||||
std::time::Duration::from_secs(60);
|
||||
const ONLINE_PROFILE_LIVE_STATE_MAX_AGE: std::time::Duration =
|
||||
std::time::Duration::from_secs(5);
|
||||
const ONLINE_PROFILE_AUTH_ERROR_BACKOFF: std::time::Duration =
|
||||
std::time::Duration::from_secs(60);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum OnlineProfileCacheIntent {
|
||||
NormalRead,
|
||||
LiveStateRead,
|
||||
RefreshFromMojang,
|
||||
}
|
||||
|
||||
impl OnlineProfileCacheIntent {
|
||||
fn max_age(self) -> std::time::Duration {
|
||||
match self {
|
||||
Self::NormalRead => ONLINE_PROFILE_CACHE_MAX_AGE,
|
||||
Self::LiveStateRead => ONLINE_PROFILE_LIVE_STATE_MAX_AGE,
|
||||
Self::RefreshFromMojang => std::time::Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn can_use_stale_on_fetch_error(self) -> bool {
|
||||
matches!(self, Self::LiveStateRead)
|
||||
}
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Refreshes the authentication tokens for this user if they are expired, or
|
||||
/// very close to expiration.
|
||||
@@ -268,92 +295,133 @@ impl Credentials {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns online profile data when the cached copy is still recent enough.
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn online_profile(&self) -> Option<Arc<MinecraftProfile>> {
|
||||
let mut profile_cache = PROFILE_CACHE.lock().await;
|
||||
self.online_profile_with_cache_intent(
|
||||
OnlineProfileCacheIntent::NormalRead,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
loop {
|
||||
match profile_cache.entry(self.offline_profile.id) {
|
||||
Entry::Occupied(entry) => {
|
||||
match entry.get() {
|
||||
ProfileCacheEntry::Hit(profile)
|
||||
if profile.is_fresh() =>
|
||||
{
|
||||
return Some(Arc::clone(profile));
|
||||
}
|
||||
ProfileCacheEntry::Hit(_) => {
|
||||
// The profile is stale, so remove it and try again
|
||||
entry.remove();
|
||||
continue;
|
||||
}
|
||||
// Auth errors must be handled with a backoff strategy because it
|
||||
// has been experimentally found that Mojang quickly rate limits
|
||||
// the profile data endpoint on repeated attempts with bad auth
|
||||
ProfileCacheEntry::AuthErrorBackoff {
|
||||
likely_expired_token,
|
||||
last_attempt,
|
||||
} if &self.access_token != likely_expired_token
|
||||
|| Instant::now()
|
||||
.saturating_duration_since(*last_attempt)
|
||||
> std::time::Duration::from_secs(60) =>
|
||||
{
|
||||
entry.remove();
|
||||
continue;
|
||||
}
|
||||
ProfileCacheEntry::AuthErrorBackoff { .. } => {
|
||||
return None;
|
||||
}
|
||||
/// Returns profile data recent enough for skin and cape state.
|
||||
///
|
||||
/// Reuses a profile read from the last few seconds so opening the skins page
|
||||
/// does not send several identical Mojang requests.
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn online_profile_fresh(&self) -> Option<Arc<MinecraftProfile>> {
|
||||
self.online_profile_with_cache_intent(
|
||||
OnlineProfileCacheIntent::LiveStateRead,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Fetches the online profile from Mojang after a skin or cape change.
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn refresh_online_profile(
|
||||
&self,
|
||||
) -> Option<Arc<MinecraftProfile>> {
|
||||
self.online_profile_with_cache_intent(
|
||||
OnlineProfileCacheIntent::RefreshFromMojang,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn online_profile_with_cache_intent(
|
||||
&self,
|
||||
cache_intent: OnlineProfileCacheIntent,
|
||||
) -> Option<Arc<MinecraftProfile>> {
|
||||
let max_age = cache_intent.max_age();
|
||||
let stale_profile = {
|
||||
let mut profile_cache = PROFILE_CACHE.lock().await;
|
||||
let mut remove_cached_entry = false;
|
||||
|
||||
let stale_profile = if let Some(cache_entry) =
|
||||
profile_cache.get(&self.offline_profile.id)
|
||||
{
|
||||
match cache_entry {
|
||||
ProfileCacheEntry::Hit(profile)
|
||||
if profile.is_fresh(max_age) =>
|
||||
{
|
||||
return Some(Arc::clone(profile));
|
||||
}
|
||||
ProfileCacheEntry::Hit(profile) => {
|
||||
Some(Arc::clone(profile))
|
||||
}
|
||||
// Auth errors must be handled with a backoff strategy because it
|
||||
// has been experimentally found that Mojang quickly rate limits
|
||||
// the profile data endpoint on repeated attempts with bad auth
|
||||
ProfileCacheEntry::AuthErrorBackoff {
|
||||
likely_expired_token,
|
||||
last_attempt,
|
||||
} if &self.access_token != likely_expired_token
|
||||
|| Instant::now()
|
||||
.saturating_duration_since(*last_attempt)
|
||||
> ONLINE_PROFILE_AUTH_ERROR_BACKOFF =>
|
||||
{
|
||||
remove_cached_entry = true;
|
||||
None
|
||||
}
|
||||
ProfileCacheEntry::AuthErrorBackoff { .. } => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
match minecraft_profile(&self.access_token).await {
|
||||
Ok(profile) => {
|
||||
let profile = Arc::new(profile);
|
||||
let cache_entry =
|
||||
ProfileCacheEntry::Hit(Arc::clone(&profile));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// When fetching a profile for the first time, the player UUID may
|
||||
// be unknown (i.e., set to a dummy value), so make sure we don't
|
||||
// cache it in the wrong place
|
||||
if entry.key() != &profile.id {
|
||||
profile_cache.insert(profile.id, cache_entry);
|
||||
} else {
|
||||
entry.insert(cache_entry);
|
||||
}
|
||||
if remove_cached_entry {
|
||||
profile_cache.remove(&self.offline_profile.id);
|
||||
}
|
||||
|
||||
return Some(profile);
|
||||
}
|
||||
Err(
|
||||
err @ MinecraftAuthenticationError::DeserializeResponse {
|
||||
status_code: StatusCode::UNAUTHORIZED,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
tracing::warn!(
|
||||
"Failed to fetch online profile for UUID {} likely due to stale credentials, backing off: {err}",
|
||||
self.offline_profile.id
|
||||
);
|
||||
stale_profile
|
||||
};
|
||||
|
||||
// We have to assume the player UUID key we have is correct here, which
|
||||
// should always be the case assuming a non-adversarial server. In any
|
||||
// case, any cache poisoning is inconsequential due to the entry expiration
|
||||
// and the fact that we use at most one single dummy UUID
|
||||
entry.insert(ProfileCacheEntry::AuthErrorBackoff {
|
||||
likely_expired_token: self.access_token.clone(),
|
||||
last_attempt: Instant::now(),
|
||||
});
|
||||
match minecraft_profile(&self.access_token).await {
|
||||
Ok(profile) => {
|
||||
let profile = Arc::new(profile);
|
||||
let cache_entry = ProfileCacheEntry::Hit(Arc::clone(&profile));
|
||||
|
||||
return None;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to fetch online profile for UUID {}: {err}",
|
||||
self.offline_profile.id
|
||||
);
|
||||
let mut profile_cache = PROFILE_CACHE.lock().await;
|
||||
if self.offline_profile.id != profile.id {
|
||||
profile_cache.remove(&self.offline_profile.id);
|
||||
}
|
||||
profile_cache.insert(profile.id, cache_entry);
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(profile)
|
||||
}
|
||||
Err(
|
||||
err @ MinecraftAuthenticationError::DeserializeResponse {
|
||||
status_code: StatusCode::UNAUTHORIZED,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
tracing::warn!(
|
||||
"Failed to fetch online profile for UUID {} likely due to stale credentials, backing off: {err}",
|
||||
self.offline_profile.id
|
||||
);
|
||||
|
||||
let mut profile_cache = PROFILE_CACHE.lock().await;
|
||||
profile_cache.insert(
|
||||
self.offline_profile.id,
|
||||
ProfileCacheEntry::AuthErrorBackoff {
|
||||
likely_expired_token: self.access_token.clone(),
|
||||
last_attempt: Instant::now(),
|
||||
},
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to fetch online profile for UUID {}: {err}",
|
||||
self.offline_profile.id
|
||||
);
|
||||
|
||||
if cache_intent.can_use_stale_on_fetch_error() {
|
||||
stale_profile
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,6 +785,8 @@ impl DeviceTokenPair {
|
||||
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
pub const MINECRAFT_SERVICES_USER_AGENT: &str =
|
||||
"Modrinth App (support@modrinth.com; https://modrinth.com/app)";
|
||||
|
||||
pub struct RequestWithDate<T> {
|
||||
pub date: DateTime<Utc>,
|
||||
@@ -1051,6 +1121,7 @@ async fn minecraft_token(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.post("https://api.minecraftservices.com/launcher/login")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
|
||||
.json(&json!({
|
||||
"platform": "PC_LAUNCHER",
|
||||
"xtoken": format!("XBL3.0 x={uhs};{token}"),
|
||||
@@ -1224,10 +1295,10 @@ impl MinecraftProfile {
|
||||
/// from the Mojang API: the vanilla launcher was seen refreshing profile
|
||||
/// data every 60 seconds when re-entering the skin selection screen, and
|
||||
/// external applications may change this data at any time.
|
||||
fn is_fresh(&self) -> bool {
|
||||
fn is_fresh(&self, max_age: std::time::Duration) -> bool {
|
||||
self.fetch_time.is_some_and(|last_profile_fetch_time| {
|
||||
Instant::now().saturating_duration_since(last_profile_fetch_time)
|
||||
< std::time::Duration::from_secs(60)
|
||||
< max_age
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1279,6 +1350,7 @@ async fn minecraft_profile(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
|
||||
.bearer_auth(token)
|
||||
// Profiles may be refreshed periodically in response to user actions,
|
||||
// so we want each refresh to be fast
|
||||
@@ -1327,12 +1399,13 @@ async fn minecraft_entitlements(
|
||||
token: &str,
|
||||
) -> Result<MinecraftEntitlements, MinecraftAuthenticationError> {
|
||||
let res = auth_retry(|| {
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
})
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
})
|
||||
.await.map_err(|source| MinecraftAuthenticationError::Request { source, step: MinecraftAuthStep::MinecraftEntitlements })?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
@@ -5,81 +5,31 @@ use super::MinecraftSkinVariant;
|
||||
|
||||
pub mod mojang_api;
|
||||
|
||||
/// Represents the default cape for a Minecraft player.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DefaultMinecraftCape {
|
||||
/// The UUID of a cape for a Minecraft player, which comes from its profile.
|
||||
///
|
||||
/// This UUID may or may not be different for every player, even if they refer to the same cape.
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
impl DefaultMinecraftCape {
|
||||
pub async fn set(
|
||||
minecraft_user_id: Uuid,
|
||||
cape_id: Uuid,
|
||||
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let minecraft_user_id = minecraft_user_id.as_hyphenated();
|
||||
let cape_id = cape_id.as_hyphenated();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO default_minecraft_capes (minecraft_user_uuid, id) VALUES (?, ?)",
|
||||
minecraft_user_id, cape_id
|
||||
)
|
||||
.execute(&mut *db.acquire().await?)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
minecraft_user_id: Uuid,
|
||||
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<Option<Self>> {
|
||||
let minecraft_user_id = minecraft_user_id.as_hyphenated();
|
||||
|
||||
Ok(sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id AS 'id: Hyphenated' FROM default_minecraft_capes WHERE minecraft_user_uuid = ?",
|
||||
minecraft_user_id
|
||||
)
|
||||
.fetch_optional(&mut *db.acquire().await?)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
minecraft_user_id: Uuid,
|
||||
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let minecraft_user_id = minecraft_user_id.as_hyphenated();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM default_minecraft_capes WHERE minecraft_user_uuid = ?",
|
||||
minecraft_user_id
|
||||
)
|
||||
.execute(&mut *db.acquire().await?)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a custom skin for a Minecraft player.
|
||||
/// Represents a saved skin row for a Minecraft player.
|
||||
///
|
||||
/// The same player and `texture_key` always point to the same saved skin.
|
||||
/// Changing the model variant or cape updates that saved skin instead of
|
||||
/// creating a second copy. Bundled default skins with a cape are also stored
|
||||
/// here so the cape can stay associated with the default skin card.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomMinecraftSkin {
|
||||
/// The key for the texture skin, which is akin to a hash that identifies it.
|
||||
/// The key for the skin texture, which is akin to a hash that identifies it.
|
||||
pub texture_key: String,
|
||||
/// The variant of the skin model.
|
||||
pub variant: MinecraftSkinVariant,
|
||||
/// The UUID of the cape that this skin uses, which should match one of the
|
||||
/// cape UUIDs the player has in its profile.
|
||||
///
|
||||
/// If `None`, the skin does not have an explicit cape set, and the default
|
||||
/// cape for this player, if any, should be used.
|
||||
/// If `None`, the skin is saved without a cape.
|
||||
pub cape_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
struct CustomMinecraftSkinRow {
|
||||
texture_key: String,
|
||||
variant: MinecraftSkinVariant,
|
||||
cape_id: Option<Hyphenated>,
|
||||
}
|
||||
|
||||
impl CustomMinecraftSkin {
|
||||
pub async fn add(
|
||||
minecraft_user_id: Uuid,
|
||||
@@ -95,24 +45,59 @@ impl CustomMinecraftSkin {
|
||||
let mut transaction = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO custom_minecraft_skin_textures (texture_key, texture) VALUES (?, ?)",
|
||||
texture_key, texture
|
||||
"DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid = ? AND texture_key = ?",
|
||||
minecraft_user_id,
|
||||
texture_key
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO custom_minecraft_skins (minecraft_user_uuid, texture_key, variant, cape_id) VALUES (?, ?, ?, ?)",
|
||||
minecraft_user_id, texture_key, variant, cape_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
"INSERT OR REPLACE INTO custom_minecraft_skin_textures (texture_key, texture) VALUES (?, ?)",
|
||||
texture_key, texture
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO custom_minecraft_skins (minecraft_user_uuid, texture_key, variant, cape_id) VALUES (?, ?, ?, ?)",
|
||||
minecraft_user_id, texture_key, variant, cape_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_by_texture(
|
||||
minecraft_user_id: Uuid,
|
||||
texture_key: &str,
|
||||
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<Option<Self>> {
|
||||
let minecraft_user_id = minecraft_user_id.as_hyphenated();
|
||||
|
||||
sqlx::query_as!(
|
||||
CustomMinecraftSkinRow,
|
||||
"SELECT texture_key, variant AS 'variant: MinecraftSkinVariant', cape_id AS 'cape_id: Hyphenated' \
|
||||
FROM custom_minecraft_skins \
|
||||
WHERE minecraft_user_uuid = ? AND texture_key = ?",
|
||||
minecraft_user_id,
|
||||
texture_key
|
||||
)
|
||||
.fetch_optional(&mut *db.acquire().await?)
|
||||
.await?
|
||||
.map(|row| {
|
||||
Ok(Self {
|
||||
texture_key: row.texture_key,
|
||||
variant: row.variant,
|
||||
cape_id: row.cape_id.map(Uuid::from),
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn get_many(
|
||||
minecraft_user_id: Uuid,
|
||||
offset: u32,
|
||||
@@ -165,12 +150,11 @@ impl CustomMinecraftSkin {
|
||||
db: impl sqlx::Acquire<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let minecraft_user_id = minecraft_user_id.as_hyphenated();
|
||||
let cape_id = self.cape_id.map(|id| id.hyphenated());
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM custom_minecraft_skins \
|
||||
WHERE minecraft_user_uuid = ? AND texture_key = ? AND variant = ? AND cape_id IS ?",
|
||||
minecraft_user_id, self.texture_key, self.variant, cape_id
|
||||
"DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid = ? AND texture_key = ?",
|
||||
minecraft_user_id,
|
||||
self.texture_key
|
||||
)
|
||||
.execute(&mut *db.acquire().await?)
|
||||
.await?;
|
||||
|
||||
@@ -10,7 +10,10 @@ use super::MinecraftSkinVariant;
|
||||
use crate::{
|
||||
ErrorKind,
|
||||
data::Credentials,
|
||||
state::{MinecraftProfile, PROFILE_CACHE, ProfileCacheEntry},
|
||||
state::{
|
||||
MINECRAFT_SERVICES_USER_AGENT, MinecraftProfile, PROFILE_CACHE,
|
||||
ProfileCacheEntry,
|
||||
},
|
||||
util::fetch::INSECURE_REQWEST_CLIENT,
|
||||
};
|
||||
|
||||
@@ -24,12 +27,13 @@ impl MinecraftCapeOperation {
|
||||
) -> crate::Result<()> {
|
||||
update_profile_cache_from_response(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.put("https://api.minecraftservices.com/minecraft/profile/capes/active")
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&credentials.access_token)
|
||||
.json(&json!({
|
||||
"capeId": cape_id.hyphenated(),
|
||||
.put("https://api.minecraftservices.com/minecraft/profile/capes/active")
|
||||
.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
|
||||
@@ -42,12 +46,13 @@ impl MinecraftCapeOperation {
|
||||
|
||||
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
|
||||
update_profile_cache_from_response(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.delete("https://api.minecraftservices.com/minecraft/profile/capes/active")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&credentials.access_token)
|
||||
.send()
|
||||
.await
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.delete("https://api.minecraftservices.com/minecraft/profile/capes/active")
|
||||
.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;
|
||||
@@ -64,7 +69,7 @@ impl MinecraftSkinOperation {
|
||||
credentials: &Credentials,
|
||||
texture: TextureStream,
|
||||
variant: MinecraftSkinVariant,
|
||||
) -> crate::Result<()>
|
||||
) -> crate::Result<Option<Arc<MinecraftProfile>>>
|
||||
where
|
||||
TextureStream: TryStream + Send + 'static,
|
||||
TextureStream::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
@@ -91,12 +96,13 @@ impl MinecraftSkinOperation {
|
||||
.file_name("skin.png"),
|
||||
);
|
||||
|
||||
update_profile_cache_from_response(
|
||||
let profile = update_profile_cache_from_response(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.post(
|
||||
"https://api.minecraftservices.com/minecraft/profile/skins",
|
||||
)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", MINECRAFT_SERVICES_USER_AGENT)
|
||||
.bearer_auth(&credentials.access_token)
|
||||
.multipart(form)
|
||||
.send()
|
||||
@@ -105,17 +111,18 @@ impl MinecraftSkinOperation {
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn unequip_any(credentials: &Credentials) -> crate::Result<()> {
|
||||
update_profile_cache_from_response(
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.delete("https://api.minecraftservices.com/minecraft/profile/skins/active")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(&credentials.access_token)
|
||||
.send()
|
||||
.await
|
||||
INSECURE_REQWEST_CLIENT
|
||||
.delete("https://api.minecraftservices.com/minecraft/profile/skins/active")
|
||||
.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;
|
||||
@@ -124,19 +131,24 @@ impl MinecraftSkinOperation {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_profile_cache_from_response(response: reqwest::Response) {
|
||||
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;
|
||||
return None;
|
||||
};
|
||||
|
||||
profile.fetch_time = Some(Instant::now());
|
||||
let profile = Arc::new(profile);
|
||||
|
||||
PROFILE_CACHE
|
||||
.lock()
|
||||
.await
|
||||
.insert(profile.id, ProfileCacheEntry::Hit(Arc::new(profile)));
|
||||
.insert(profile.id, ProfileCacheEntry::Hit(Arc::clone(&profile)));
|
||||
|
||||
Some(profile)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user