diff --git a/LaunchServer/source/texture/ElyByTextureProvider.java b/LaunchServer/source/texture/ElyByTextureProvider.java index ce3697e..e0ef6b1 100644 --- a/LaunchServer/source/texture/ElyByTextureProvider.java +++ b/LaunchServer/source/texture/ElyByTextureProvider.java @@ -1,13 +1,36 @@ package launchserver.texture; +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +import launcher.LauncherAPI; import launcher.client.PlayerProfile.Texture; +import launcher.helper.IOHelper; +import launcher.helper.JVMHelper; +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; import launcher.serialize.config.entry.BlockConfigEntry; +import launchserver.helpers.HTTPRequestHelper; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +// Пардон, но я их рот топтал +// Я лучше вынесу полный класс с логикой сюда, чтобы это все не пошло по косяку public class ElyByTextureProvider extends TextureProvider { - protected CacheTextureProvider cacheTextureProvider; + @LauncherAPI + public static final long CACHE_DURATION_MS = VerifyHelper.verifyLong( + Long.parseLong(System.getProperty("launcher.mysql.cacheDurationHours", Integer.toString(24))), + VerifyHelper.L_NOT_NEGATIVE, "launcher.mysql.cacheDurationHours can't be < 0") * 60L * 60L * 1000L; + + // Instance + private final Map cache = new HashMap<>(1024); public ElyByTextureProvider(BlockConfigEntry block) { @@ -20,15 +43,129 @@ // Do nothing } + private CacheData getCached(UUID uuid, String username, String in_usersURL, String in_profileURL, String serviceName) + { + CacheData result = cache.get(username); + + // Have cached result? + if (result != null && System.currentTimeMillis() < result.until) + { + if (result.exc != null) + { + JVMHelper.UNSAFE.throwException(result.exc); + } + return result; + } + + try + { + // TODO Don't query UUID by username if using mojang auth handler (not implemented yet) + URL uuidURL = new URL(in_usersURL + IOHelper.urlEncode(username)); + JsonObject uuidResponse = HTTPRequestHelper.makeAuthlibRequest(uuidURL, null, serviceName); + if (uuidResponse == null) + { + throw new IllegalArgumentException("Empty UUID response"); + } + String uuidResolved = uuidResponse.get("id").asString(); + + // Obtain player profile + URL profileURL = new URL(in_profileURL + username); // ... + // Я просто выпилю эту систему, если еще такие приколы найду: + // https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + // https://docs.ely.by/ru/skins-system.html => /profile/{nickname} + JsonObject profileResponse = HTTPRequestHelper.makeAuthlibRequest(profileURL, null, serviceName); + if (profileResponse == null) + { + throw new IllegalArgumentException("Empty Authlib response"); + } + JsonArray properties = (JsonArray) profileResponse.get("properties"); + if (properties == null) + { + LogHelper.subDebug("No properties"); + return cache(username, null, null, null); + } + + // Find textures property + JsonObject texturesProperty = null; + for (JsonValue property : properties) + { + JsonObject property0 = property.asObject(); + if (property0.get("name").asString().equals("textures")) + { + byte[] asBytes = Base64.getDecoder().decode(property0.get("value").asString()); + String asString = new String(asBytes, StandardCharsets.UTF_8); + texturesProperty = Json.parse(asString).asObject(); + break; + } + } + if (texturesProperty == null) + { + LogHelper.subDebug("No textures property"); + return cache(username, null, null, null); + } + + // Extract skin&cloak texture + texturesProperty = (JsonObject) texturesProperty.get("textures"); + JsonObject skinProperty = (JsonObject) texturesProperty.get("SKIN"); + Texture skinTexture = skinProperty == null ? null : new Texture(skinProperty.get("url").asString(), false); + JsonObject cloakProperty = (JsonObject) texturesProperty.get("CAPE"); + Texture cloakTexture = cloakProperty == null ? null : new Texture(cloakProperty.get("url").asString(), true); + + // We're done + return cache(username, skinTexture, cloakTexture, null); + } + catch (Throwable exc) + { + cache(username, null, null, exc); + JVMHelper.UNSAFE.throwException(exc); + } + + // We're dones + return result; + } + + private CacheData cache(String username, Texture skin, Texture cloak, Throwable exc) + { + long until = CACHE_DURATION_MS == 0L ? Long.MIN_VALUE : System.currentTimeMillis() + CACHE_DURATION_MS; + CacheData data = exc == null ? new CacheData(skin, cloak, until) : new CacheData(exc, until); + if (CACHE_DURATION_MS != 0L) + { + cache.put(username, data); + } + return data; + } + + private static final class CacheData + { + protected final Texture skin, cloak; + protected final Throwable exc; + protected final long until; + + private CacheData(Texture skin, Texture cloak, long until) + { + this.skin = skin; + this.cloak = cloak; + this.until = until; + exc = null; + } + + private CacheData(Throwable exc, long until) + { + this.exc = exc; + this.until = until; + skin = cloak = null; + } + } + @Override public synchronized Texture getSkinTexture(UUID uuid, String username) { - return cacheTextureProvider.getCached(uuid, username, "https://authserver.ely.by/api/users/profiles/minecraft/", "http://skinsystem.ely.by/profile/", "ElyBy").skin; + return getCached(uuid, username, "https://authserver.ely.by/api/users/profiles/minecraft/", "http://skinsystem.ely.by/profile/", "ElyBy").skin; } @Override public synchronized Texture getCloakTexture(UUID uuid, String username) { - return cacheTextureProvider.getCached(uuid, username, "https://authserver.ely.by/api/users/profiles/minecraft/", "http://skinsystem.ely.by/profile/", "ElyBy").cloak; + return getCached(uuid, username, "https://authserver.ely.by/api/users/profiles/minecraft/", "http://skinsystem.ely.by/profile/", "ElyBy").cloak; } }