From a05e89cf581483c1f9aa5662e0ee001cee457957 Mon Sep 17 00:00:00 2001 From: Daenges <57369924+Daenges@users.noreply.github.com> Date: Fri, 13 Aug 2021 08:27:53 +0200 Subject: [PATCH] Addition to MojangAPI.cs (#1714) * Add skin handling to MojangAPI.cs * Add new skin requests and improve commenting * Update UUID hash method to detect default player skin according to https://github.com/ORelio/Minecraft-Console-Client/pull/1714#issuecomment-894751307 * Add custom return types * Add ServiceStatus enum for the mojang services --- MinecraftClient/Protocol/MojangAPI.cs | 179 +++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 16 deletions(-) diff --git a/MinecraftClient/Protocol/MojangAPI.cs b/MinecraftClient/Protocol/MojangAPI.cs index bafbb6db..78c8e7de 100644 --- a/MinecraftClient/Protocol/MojangAPI.cs +++ b/MinecraftClient/Protocol/MojangAPI.cs @@ -6,10 +6,66 @@ using System.Net; /// By using these functions you agree to the ToS of the Mojang API. /// You should note that all public APIs are rate limited so you are expected to cache the results. /// This is currently set at 600 requests per 10 minutes but this may change. +/// Source: https://wiki.vg/Mojang_API /// !!! ATTENTION !!! namespace MinecraftClient.Protocol { + // Enum to display the status of different services + public enum ServiceStatus { red, yellow, green }; + + /// + /// Information about a players Skin. + /// Empty string if not available. + /// + public class SkinInfo + { + public readonly string SkinUrl; + public readonly string CapeUrl; + public readonly string SkinModel; + + public SkinInfo(string skinUrl = "", string capeUrl = "", string skinModel = "") + { + SkinUrl = skinUrl; + CapeUrl = capeUrl; + SkinModel = skinModel; + } + } + + /// + /// Status of the single Mojang services + /// + public class MojangServiceStatus + { + public readonly ServiceStatus MinecraftNet; + public readonly ServiceStatus SessionMinecraftNet; + public readonly ServiceStatus AccountMojangCom; + public readonly ServiceStatus AuthserverMojangCom; + public readonly ServiceStatus SessionserverMojangCom; + public readonly ServiceStatus ApiMojangCom; + public readonly ServiceStatus TexturesMinecraftNet; + public readonly ServiceStatus MojangCom; + + public MojangServiceStatus(ServiceStatus minecraftNet = ServiceStatus.red, + ServiceStatus sessionMinecraftNet = ServiceStatus.red, + ServiceStatus accountMojangCom = ServiceStatus.red, + ServiceStatus authserverMojangCom = ServiceStatus.red, + ServiceStatus sessionserverMojangCom = ServiceStatus.red, + ServiceStatus apiMojangCom = ServiceStatus.red, + ServiceStatus texturesMinecraftNet = ServiceStatus.red, + ServiceStatus mojangCom = ServiceStatus.red) + { + MinecraftNet = minecraftNet; + SessionMinecraftNet = sessionMinecraftNet; + AccountMojangCom = accountMojangCom; + AuthserverMojangCom = authserverMojangCom; + SessionserverMojangCom = sessionserverMojangCom; + ApiMojangCom = apiMojangCom; + TexturesMinecraftNet = texturesMinecraftNet; + MojangCom = mojangCom; + } + } + /// /// Provides methods to easily interact with the Mojang API. /// @@ -34,6 +90,26 @@ namespace MinecraftClient.Protocol } // Can be removed in newer C# versions. + /// + /// Converts a string to a ServiceStatus enum. + /// + /// string to convert + /// ServiceStatus enum, red as default. + private static ServiceStatus stringToServiceStatus(string s) + { + ServiceStatus servStat; + + if (Enum.TryParse(s, out servStat)) + { + return servStat; + } + else + { + // return red as standard value. + return ServiceStatus.red; + } + } + /// /// Obtain the UUID of a Player through its name /// @@ -94,7 +170,7 @@ namespace MinecraftClient.Protocol // DateTimeOffset creationDate = DateTimeOffset.FromUnixTimeMilliseconds(Convert.ToInt64(jsonData.Properties["changedToAt"].StringValue)); // - // Workaround for converting Unix time to normal time + // Workaround for converting Unix time to normal time. DateTimeOffset creationDate = unixTimeStampToDateTime(Convert.ToDouble(jsonData.Properties["changedToAt"].StringValue)); // Add Keyvaluepair to dict. @@ -115,32 +191,103 @@ namespace MinecraftClient.Protocol /// Get the Mojang API status /// /// Dictionary of the Mojang services - public static Dictionary GetMojangServiceStatus() + public static MojangServiceStatus GetMojangServiceStatus() { - Dictionary tempDict = new Dictionary(); - List jsonDataList; + List jsonDataList = new List(); // Perform web request try { jsonDataList = Json.ParseJson(wc.DownloadString("https://status.mojang.com/check")).DataArray; } - catch (Exception) { return tempDict; } + catch (Exception) { new MojangServiceStatus(); } - // Convert JSONData to string and parse it to a dictionary. - foreach (Json.JSONData jsonData in jsonDataList) + // Convert string to enum values and store them inside a MojangeServiceStatus object. + return new MojangServiceStatus(minecraftNet: stringToServiceStatus(jsonDataList[0].Properties["minecraft.net"].StringValue), + sessionMinecraftNet: stringToServiceStatus(jsonDataList[1].Properties["session.minecraft.net"].StringValue), + accountMojangCom: stringToServiceStatus(jsonDataList[2].Properties["account.mojang.com"].StringValue), + authserverMojangCom: stringToServiceStatus(jsonDataList[3].Properties["authserver.mojang.com"].StringValue), + sessionserverMojangCom: stringToServiceStatus(jsonDataList[4].Properties["sessionserver.mojang.com"].StringValue), + apiMojangCom: stringToServiceStatus(jsonDataList[5].Properties["api.mojang.com"].StringValue), + texturesMinecraftNet: stringToServiceStatus(jsonDataList[6].Properties["textures.minecraft.net"].StringValue), + mojangCom: stringToServiceStatus(jsonDataList[7].Properties["mojang.com"].StringValue) + ); + } + + /// + /// Obtain links to skin, skinmodel and cape of a player. + /// + /// UUID of a player + /// Dictionary with a link to the skin and cape of a player. + public static SkinInfo GetSkinInfo(string uuid) + { + Dictionary textureDict; + string base64SkinInfo; + Json.JSONData decodedJsonSkinInfo; + + // Perform web request + try { - if (jsonData.Properties.Count > 0) - { - foreach (KeyValuePair keyValuePair in jsonData.Properties) - { - // Service name to status - tempDict.Add(keyValuePair.Key, keyValuePair.Value.StringValue); - } - } + // Obtain the Base64 encoded skin information from the API. Discard the rest, since it can be obtained easier through other requests. + base64SkinInfo = Json.ParseJson(wc.DownloadString("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid)).Properties["properties"].DataArray[0].Properties["value"].StringValue; } + catch (Exception) { return new SkinInfo(); } - return tempDict; + // Parse the decoded string to the JSON format. + decodedJsonSkinInfo = Json.ParseJson(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64SkinInfo))); + + // Assert temporary variable for readablity. + // Contains skin and cape information. + textureDict = decodedJsonSkinInfo.Properties["textures"].Properties; + + // Can apparently be missing, if no custom skin is set. + // Probably for completely new accounts. + // (Still exists after changing back to Steve or Alex skin.) + if (textureDict.ContainsKey("SKIN")) + { + return new SkinInfo(skinUrl: textureDict["SKIN"].Properties.ContainsKey("url") ? textureDict["SKIN"].Properties["url"].StringValue : string.Empty, + capeUrl: textureDict.ContainsKey("CAPE") ? textureDict["CAPE"].Properties["url"].StringValue : string.Empty, + skinModel: textureDict["SKIN"].Properties.ContainsKey("metadata") ? "Alex" : "Steve"); + } + // Tested it on several players, this case never occured. + else + { + // This player has assumingly never changed their skin. + // Probably a completely new account. + return new SkinInfo(capeUrl: textureDict.ContainsKey("CAPE") ? textureDict["CAPE"].Properties["url"].StringValue : string.Empty, + skinModel: DefaultModelAlex(uuid) ? "Alex" : "Steve"); + } + } + + /// + /// Gets the playermodel that is assigned to the account by default. + /// (Before the skin is changed for the first time.) + /// + /// UUID of a Player + /// True if the default model for this UUID is Alex + public static bool DefaultModelAlex(string uuid) + { + return hashCode(uuid) % 2 == 1; + } + + /// + /// Creates the hash of an UUID + /// + /// UUID of a player. + /// + private static int hashCode(string hash) + { + byte[] data = GuidExtensions.ToLittleEndian(new Guid(hash)).ToByteArray(); + + ulong msb = 0; + ulong lsb = 0; + for (int i = 0; i < 8; i++) + msb = (msb << 8) | (uint)(data[i] & 0xff); + for (int i = 8; i < 16; i++) + lsb = (lsb << 8) | (uint)(data[i] & 0xff); + var hilo = msb ^ lsb; + + return ((int)(hilo >> 32)) ^ (int)hilo; } } }