Merge pull request #12 from milutinke/master

This commit is contained in:
Anon 2026-03-22 20:42:41 +01:00 committed by GitHub
commit 5c39d3b83d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 393 additions and 999 deletions

View file

@ -1,377 +1,55 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MinecraftClient
namespace MinecraftClient;
/// <summary>
/// JSON utilities backed by System.Text.Json.
/// </summary>
public static class Json
{
private static readonly JsonSerializerOptions s_escapeOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// Parse a JSON string into a mutable <see cref="JsonNode"/> DOM.
/// Returns null for null, empty, or whitespace-only input.
/// Returns a <see cref="JsonValue"/> wrapping the raw string when the input
/// is not valid JSON (e.g. a plain-text Minecraft MOTD or chat message).
/// </summary>
public static JsonNode? ParseJson(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return null;
try { return JsonNode.Parse(json); }
catch (JsonException) { return JsonValue.Create(json); }
}
/// <summary>
/// Escape a string for embedding inside a JSON string literal.
/// Uses System.Text.Json serialization and strips the surrounding quotes.
/// </summary>
public static string EscapeString(string src) =>
JsonSerializer.Serialize(src, s_escapeOptions)[1..^1];
}
/// <summary>
/// Extension helpers for <see cref="JsonNode"/> that replicate the access patterns
/// of the former <c>JSONData.StringValue</c> property.
/// </summary>
public static class JsonNodeExtensions
{
/// <summary>
/// This class parses JSON data and returns an object describing that data.
/// Really lightweight JSON handling by ORelio - (c) 2013 - 2020
/// Return the string representation of any JSON value.
/// Strings are returned without quotes; numbers, booleans, and null
/// are returned as their text representation.
/// </summary>
public static class Json
public static string GetStringValue(this JsonNode? node) => node switch
{
/// <summary>
/// Parse some JSON and return the corresponding JSON object
/// </summary>
public static JSONData ParseJson(string json)
{
int cursorpos = 0;
return String2Data(json, ref cursorpos);
}
/// <summary>
/// The class storing unserialized JSON data
/// The data can be an object, an array or a string
/// </summary>
public class JSONData
{
public enum DataType
{
Object,
Array,
String
};
private readonly DataType type;
public DataType Type
{
get { return type; }
}
public Dictionary<string, JSONData> Properties;
public List<JSONData> DataArray;
public string StringValue;
public JSONData(DataType datatype)
{
type = datatype;
Properties = new Dictionary<string, JSONData>();
DataArray = new List<JSONData>();
StringValue = String.Empty;
}
}
/// <summary>
/// Parse a JSON string to build a JSON object
/// </summary>
/// <param name="toparse">String to parse</param>
/// <param name="cursorpos">Cursor start (set to 0 for function init)</param>
private static JSONData String2Data(string toparse, ref int cursorpos)
{
try
{
JSONData data;
SkipSpaces(toparse, ref cursorpos);
switch (toparse[cursorpos])
{
//Object
case '{':
data = new JSONData(JSONData.DataType.Object);
cursorpos++;
SkipSpaces(toparse, ref cursorpos);
while (toparse[cursorpos] != '}')
{
if (toparse[cursorpos] == '"')
{
JSONData propertyname = String2Data(toparse, ref cursorpos);
if (toparse[cursorpos] == ':')
{
cursorpos++;
}
else
{
/* parse error ? */
}
JSONData propertyData = String2Data(toparse, ref cursorpos);
data.Properties[propertyname.StringValue] = propertyData;
}
else cursorpos++;
}
cursorpos++;
break;
//Array
case '[':
data = new JSONData(JSONData.DataType.Array);
cursorpos++;
SkipSpaces(toparse, ref cursorpos);
while (toparse[cursorpos] != ']')
{
if (toparse[cursorpos] == ',')
{
cursorpos++;
}
JSONData arrayItem = String2Data(toparse, ref cursorpos);
data.DataArray.Add(arrayItem);
}
cursorpos++;
break;
//String
case '"':
data = new JSONData(JSONData.DataType.String);
cursorpos++;
while (toparse[cursorpos] != '"')
{
if (toparse[cursorpos] == '\\')
{
try //Unicode character \u0123
{
if (toparse[cursorpos + 1] == 'u'
&& IsHex(toparse[cursorpos + 2])
&& IsHex(toparse[cursorpos + 3])
&& IsHex(toparse[cursorpos + 4])
&& IsHex(toparse[cursorpos + 5]))
{
//"abc\u0123abc" => "0123" => 0123 => Unicode char n°0123 => Add char to string
data.StringValue += char.ConvertFromUtf32(int.Parse(
toparse.Substring(cursorpos + 2, 4),
System.Globalization.NumberStyles.HexNumber));
cursorpos += 6;
continue;
}
else if (toparse[cursorpos + 1] == 'n')
{
data.StringValue += '\n';
cursorpos += 2;
continue;
}
else if (toparse[cursorpos + 1] == 'r')
{
data.StringValue += '\r';
cursorpos += 2;
continue;
}
else if (toparse[cursorpos + 1] == 't')
{
data.StringValue += '\t';
cursorpos += 2;
continue;
}
else cursorpos++; //Normal character escapement \"
}
catch (IndexOutOfRangeException)
{
cursorpos++;
} // \u01<end of string>
catch (ArgumentOutOfRangeException)
{
cursorpos++;
} // Unicode index 0123 was invalid
}
data.StringValue += toparse[cursorpos];
cursorpos++;
}
cursorpos++;
break;
//Number
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '.':
case '-':
data = new JSONData(JSONData.DataType.String);
StringBuilder sb = new();
while ((toparse[cursorpos] >= '0' && toparse[cursorpos] <= '9') || toparse[cursorpos] == '.' ||
toparse[cursorpos] == '-')
{
sb.Append(toparse[cursorpos]);
cursorpos++;
}
data.StringValue = sb.ToString();
break;
//Boolean : true
case 't':
data = new JSONData(JSONData.DataType.String);
cursorpos++;
if (toparse[cursorpos] == 'r')
{
cursorpos++;
}
if (toparse[cursorpos] == 'u')
{
cursorpos++;
}
if (toparse[cursorpos] == 'e')
{
cursorpos++;
data.StringValue = "true";
}
break;
//Boolean : false
case 'f':
data = new JSONData(JSONData.DataType.String);
cursorpos++;
if (toparse[cursorpos] == 'a')
{
cursorpos++;
}
if (toparse[cursorpos] == 'l')
{
cursorpos++;
}
if (toparse[cursorpos] == 's')
{
cursorpos++;
}
if (toparse[cursorpos] == 'e')
{
cursorpos++;
data.StringValue = "false";
}
break;
//Null field
case 'n':
data = new JSONData(JSONData.DataType.String);
cursorpos++;
if (toparse[cursorpos] == 'u')
{
cursorpos++;
}
if (toparse[cursorpos] == 'l')
{
cursorpos++;
}
if (toparse[cursorpos] == 'l')
{
cursorpos++;
data.StringValue = "null";
}
break;
//Unknown data
default:
cursorpos++;
return String2Data(toparse, ref cursorpos);
}
SkipSpaces(toparse, ref cursorpos);
return data;
}
catch (IndexOutOfRangeException)
{
return new JSONData(JSONData.DataType.String);
}
}
/// <summary>
/// Check if a char is an hexadecimal char (0-9 A-F a-f)
/// </summary>
/// <param name="c">Char to test</param>
/// <returns>True if hexadecimal</returns>
private static bool IsHex(char c)
{
return ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));
}
/// <summary>
/// Advance the cursor to skip white spaces and line breaks
/// </summary>
/// <param name="toparse">String to parse</param>
/// <param name="cursorpos">Cursor position to update</param>
private static void SkipSpaces(string toparse, ref int cursorpos)
{
while (cursorpos < toparse.Length
&& (char.IsWhiteSpace(toparse[cursorpos])
|| toparse[cursorpos] == '\r'
|| toparse[cursorpos] == '\n'))
cursorpos++;
}
// Original: https://github.com/mono/mono/blob/master/mcs/class/System.Json/System.Json/JsonValue.cs
private static bool NeedEscape(string src, int i)
{
var c = src[i];
return c < 32 || c == '"' || c == '\\'
// Broken lead surrogate
|| (c is >= '\uD800' and <= '\uDBFF' &&
(i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF'))
// Broken tail surrogate
|| (c is >= '\uDC00' and <= '\uDFFF' &&
(i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF'))
// To produce valid JavaScript
|| c == '\u2028' || c == '\u2029'
// Escape "</" for <script> tags
|| (c == '/' && i > 0 && src[i - 1] == '<');
}
public static string EscapeString(string src)
{
var sb = new StringBuilder();
var start = 0;
for (var i = 0; i < src.Length; i++)
{
if (!NeedEscape(src, i)) continue;
sb.Append(src, start, i - start);
switch (src[i])
{
case '\b':
sb.Append("\\b");
break;
case '\f':
sb.Append("\\f");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
case '\"':
sb.Append("\\\"");
break;
case '\\':
sb.Append("\\\\");
break;
case '/':
sb.Append("\\/");
break;
default:
sb.Append("\\u");
sb.Append(((int)src[i]).ToString("x04"));
break;
}
start = i + 1;
}
sb.Append(src, start, src.Length - start);
return sb.ToString();
}
}
null => "null",
JsonValue val when val.TryGetValue<string>(out var s) => s,
_ => node.ToJsonString()
};
}

View file

@ -51,8 +51,8 @@ namespace MinecraftClient.Mapping.BlockPalettes
HashSet<int> knownStates = new();
Dictionary<string, HashSet<int>> blocks = new();
Json.JSONData palette = Json.ParseJson(File.ReadAllText(blocksJsonFile, Encoding.UTF8));
foreach (KeyValuePair<string, Json.JSONData> item in palette.Properties)
var palette = Json.ParseJson(File.ReadAllText(blocksJsonFile, Encoding.UTF8))!.AsObject();
foreach (var item in palette)
{
//minecraft:item_name => ItemName
string blockType = String.Concat(
@ -65,9 +65,9 @@ namespace MinecraftClient.Mapping.BlockPalettes
throw new InvalidDataException("Duplicate block type " + blockType + "!?");
blocks[blockType] = new HashSet<int>();
foreach (Json.JSONData state in item.Value.Properties["states"].DataArray)
foreach (var state in item.Value!["states"]!.AsArray())
{
int id = int.Parse(state.Properties["id"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
int id = int.Parse(state!["id"].GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture);
if (knownStates.Contains(id))
throw new InvalidDataException("Duplicate state id " + id + "!?");

View file

@ -21,13 +21,13 @@ namespace MinecraftClient.Protocol
/// <returns></returns>
private static Dictionary<int, string> LoadRegistry(string registriesJsonFile, string jsonRegistryName)
{
Json.JSONData rawJson = Json.ParseJson(File.ReadAllText(registriesJsonFile));
Json.JSONData rawRegistry = rawJson.Properties[jsonRegistryName].Properties["entries"];
var rawJson = Json.ParseJson(File.ReadAllText(registriesJsonFile));
var rawRegistry = rawJson![jsonRegistryName]!["entries"]!.AsObject();
Dictionary<int, string> registry = new();
foreach (KeyValuePair<string, Json.JSONData> entry in rawRegistry.Properties)
foreach (var entry in rawRegistry)
{
int entryId = int.Parse(entry.Value.Properties["protocol_id"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
int entryId = int.Parse(entry.Value!["protocol_id"].GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture);
//minecraft:item_name => ItemName
string entryName = String.Concat(

View file

@ -63,7 +63,7 @@ namespace MinecraftClient.Protocol.Handlers.Forge
/// </summary>
/// <param name="data">The modinfo JSON tag.</param>
/// <param name="fmlVersion">Forge protocol version</param>
internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion)
internal ForgeInfo(System.Text.Json.Nodes.JsonObject data, FMLVersion fmlVersion)
{
Mods = new List<ForgeMod>();
Version = fmlVersion;
@ -91,10 +91,10 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// }]
// }
foreach (Json.JSONData mod in data.Properties["modList"].DataArray)
foreach (var mod in data["modList"]!.AsArray())
{
String modid = mod.Properties["modid"].StringValue;
String modversion = mod.Properties["version"].StringValue;
String modid = mod!["modid"]!.GetStringValue();
String modversion = mod["version"]!.GetStringValue();
Mods.Add(new ForgeMod(modid, modversion));
}
@ -131,10 +131,10 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// "fmlNetworkVersion": 2
// }
foreach (Json.JSONData mod in data.Properties["mods"].DataArray)
foreach (var mod in data["mods"]!.AsArray())
{
String modid = mod.Properties["modId"].StringValue;
String modmarker = mod.Properties["modmarker"].StringValue;
String modid = mod!["modId"]!.GetStringValue();
String modmarker = mod["modmarker"]!.GetStringValue();
Mods.Add(new ForgeMod(modid, modmarker));
}
@ -157,7 +157,7 @@ namespace MinecraftClient.Protocol.Handlers.Forge
// - Here is the discussion:
// see https://github.com/MinecraftForge/MinecraftForge/pull/8169
string encodedData = data.Properties["d"].StringValue;
string encodedData = data["d"]!.GetStringValue();
Queue<byte> dataPackage = decodeOptimized(encodedData);
DataTypes dataTypes = new DataTypes(Protocol18Handler.MC_1_18_1_Version);

View file

@ -1031,10 +1031,10 @@ namespace MinecraftClient.Protocol.Handlers
? dataTypes.ReadNextString(packetData)
: null;
var chatInfo = Json.ParseJson(chatName).Properties;
var senderDisplayName = chatInfo != null && chatInfo.Count > 0
var chatInfo = Json.ParseJson(chatName)?.AsObject();
var senderDisplayName = chatInfo is not null && chatInfo.Count > 0
? (chatInfo.ContainsKey("insertion") ? chatInfo["insertion"] : chatInfo["text"])
.StringValue
.GetStringValue()
: "";
string? senderTeamName = null;
var messageTypeEnum =
@ -1043,8 +1043,8 @@ namespace MinecraftClient.Protocol.Handlers
if (targetName != null &&
(messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_INCOMING ||
messageTypeEnum == ChatParser.MessageType.TEAM_MSG_COMMAND_OUTGOING))
senderTeamName = Json.ParseJson(targetName).Properties["with"].DataArray[0]
.Properties["text"].StringValue;
senderTeamName = Json.ParseJson(targetName)!["with"]![0]!
["text"]!.GetStringValue();
if (string.IsNullOrWhiteSpace(senderDisplayName))
{
@ -3574,22 +3574,22 @@ namespace MinecraftClient.Protocol.Handlers
if (string.IsNullOrEmpty(result) || !result.StartsWith("{") || !result.EndsWith("}")) return false;
var jsonData = Json.ParseJson(result);
if (jsonData.Type != Json.JSONData.DataType.Object || !jsonData.Properties.ContainsKey("version"))
if (jsonData is not System.Text.Json.Nodes.JsonObject jsonObj || !jsonObj.ContainsKey("version"))
return false;
var versionData = jsonData.Properties["version"];
var versionData = jsonObj["version"]!.AsObject();
//Retrieve display name of the Minecraft version
if (versionData.Properties.TryGetValue("name", out var property))
version = property.StringValue;
if (versionData["name"] is { } nameNode)
version = nameNode.GetStringValue();
//Retrieve protocol version number for handling this server
if (versionData.Properties.TryGetValue("protocol", out var dataProperty))
protocolVersion = int.Parse(dataProperty.StringValue,
if (versionData["protocol"] is { } protocolNode)
protocolVersion = int.Parse(protocolNode.GetStringValue(),
NumberStyles.Any, CultureInfo.CurrentCulture);
// Check for forge on the server.
Protocol18Forge.ServerInfoCheckForge(jsonData, ref forgeInfo);
Protocol18Forge.ServerInfoCheckForge(jsonObj, ref forgeInfo);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version,
protocolVersion + (forgeInfo != null ? Translations.mcc_with_forge : "")));

View file

@ -484,7 +484,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo)
public static bool ServerInfoCheckForge(System.Text.Json.Nodes.JsonObject jsonData, ref ForgeInfo? forgeInfo)
{
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2) // MC 1.13 to 1.17
@ -530,7 +530,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
private static bool ServerInfoCheckForgeSub(System.Text.Json.Nodes.JsonObject jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
{
string forgeDataTag;
string versionField;
@ -557,10 +557,9 @@ namespace MinecraftClient.Protocol.Handlers
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
if (jsonData[forgeDataTag] is System.Text.Json.Nodes.JsonObject modData)
{
Json.JSONData modData = jsonData.Properties[forgeDataTag];
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
if (modData[versionField] is not null && modData[versionField]!.GetStringValue() == versionString)
{
forgeInfo = new ForgeInfo(modData, fmlVersion);
if (forgeInfo.Mods.Any())

View file

@ -436,74 +436,70 @@ namespace MinecraftClient.Protocol.Message
/// <param name="colorcode">Allow parent color code to affect child elements (set to "" for function init)</param>
/// <param name="links">Container for links from JSON serialized text</param>
/// <returns>returns the Minecraft-formatted string</returns>
private static string JSONData2String(Json.JSONData data, string colorcode, List<string>? links)
private static string JSONData2String(System.Text.Json.Nodes.JsonNode? data, string colorcode, List<string>? links)
{
string extra_result = "";
switch (data.Type)
switch (data)
{
case Json.JSONData.DataType.Object:
if (data.Properties.ContainsKey("color"))
case System.Text.Json.Nodes.JsonObject obj:
if (obj.ContainsKey("color"))
{
colorcode = Color2tag(JSONData2String(data.Properties["color"], "", links));
colorcode = Color2tag(JSONData2String(obj["color"], "", links));
}
if (data.Properties.ContainsKey("clickEvent") && links != null)
if (obj.ContainsKey("clickEvent") && links is not null)
{
Json.JSONData clickEvent = data.Properties["clickEvent"];
if (clickEvent.Properties.ContainsKey("action")
&& clickEvent.Properties.ContainsKey("value")
&& clickEvent.Properties["action"].StringValue == "open_url"
&& !string.IsNullOrEmpty(clickEvent.Properties["value"].StringValue))
var clickEvent = obj["clickEvent"]!.AsObject();
if (clickEvent.ContainsKey("action")
&& clickEvent.ContainsKey("value")
&& clickEvent["action"]!.GetStringValue() == "open_url"
&& !string.IsNullOrEmpty(clickEvent["value"]!.GetStringValue()))
{
links.Add(clickEvent.Properties["value"].StringValue);
links.Add(clickEvent["value"]!.GetStringValue());
}
}
if (data.Properties.ContainsKey("extra"))
if (obj.ContainsKey("extra"))
{
Json.JSONData[] extras = data.Properties["extra"].DataArray.ToArray();
foreach (Json.JSONData item in extras)
foreach (var item in obj["extra"]!.AsArray())
extra_result = extra_result + JSONData2String(item, colorcode, links) + "§r";
}
if (data.Properties.ContainsKey("text"))
if (obj.ContainsKey("text"))
{
return colorcode + JSONData2String(data.Properties["text"], colorcode, links) + extra_result;
return colorcode + JSONData2String(obj["text"], colorcode, links) + extra_result;
}
else if (data.Properties.ContainsKey("translate"))
else if (obj.ContainsKey("translate"))
{
List<string> using_data = new();
if (data.Properties.ContainsKey("using") && !data.Properties.ContainsKey("with"))
data.Properties["with"] = data.Properties["using"];
if (data.Properties.ContainsKey("with"))
if (obj.ContainsKey("using") && !obj.ContainsKey("with"))
obj["with"] = obj["using"]!.DeepClone();
if (obj.ContainsKey("with"))
{
Json.JSONData[] array = data.Properties["with"].DataArray.ToArray();
for (int i = 0; i < array.Length; i++)
foreach (var item in obj["with"]!.AsArray())
{
using_data.Add(JSONData2String(array[i], colorcode, links));
using_data.Add(JSONData2String(item, colorcode, links));
}
}
return colorcode +
TranslateString(JSONData2String(data.Properties["translate"], "", links), using_data) +
TranslateString(JSONData2String(obj["translate"], "", links), using_data) +
extra_result;
}
else return extra_result;
case Json.JSONData.DataType.Array:
case System.Text.Json.Nodes.JsonArray arr:
string result = "";
foreach (Json.JSONData item in data.DataArray)
foreach (var item in arr)
{
result += JSONData2String(item, colorcode, links);
}
return result;
case Json.JSONData.DataType.String:
return colorcode + data.StringValue;
default:
return colorcode + data.GetStringValue();
}
return "";
}
private static string NbtToString(Dictionary<string, object> nbt)

View file

@ -68,20 +68,20 @@ namespace MinecraftClient.Protocol
var jsonData = Json.ParseJson(response.Body);
// Error handling
if (jsonData.Properties.ContainsKey("error"))
if (jsonData?["error"] is not null)
{
throw new Exception(jsonData.Properties["error_description"].StringValue);
throw new Exception(jsonData["error_description"].GetStringValue());
}
else
{
string accessToken = jsonData.Properties["access_token"].StringValue;
string refreshToken = jsonData.Properties["refresh_token"].StringValue;
int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
string accessToken = jsonData!["access_token"]!.GetStringValue();
string refreshToken = jsonData["refresh_token"]!.GetStringValue();
int expiresIn = int.Parse(jsonData["expires_in"].GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture);
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue);
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload.Properties["email"].StringValue;
string email = jsonPayload!["email"]!.GetStringValue();
return new LoginResponse()
{
Email = email,
@ -299,9 +299,9 @@ namespace MinecraftClient.Protocol
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
var json = Json.ParseJson(jsonString);
string token = json!["Token"]!.GetStringValue();
string userHash = json["DisplayClaims"]!["xui"]![0]!["uhs"]!.GetStringValue();
return new XblAuthenticateResponse()
{
Token = token,
@ -347,9 +347,9 @@ namespace MinecraftClient.Protocol
if (response.StatusCode == 200)
{
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
var json = Json.ParseJson(jsonString);
string token = json!["Token"]!.GetStringValue();
string userHash = json["DisplayClaims"]!["xui"]![0]!["uhs"]!.GetStringValue();
return new XSTSAuthenticateResponse()
{
Token = token,
@ -360,16 +360,16 @@ namespace MinecraftClient.Protocol
{
if (response.StatusCode == 401)
{
Json.JSONData json = Json.ParseJson(response.Body);
if (json.Properties["XErr"].StringValue == "2148916233")
var json = Json.ParseJson(response.Body);
if (json!["XErr"]!.GetStringValue() == "2148916233")
{
throw new Exception("The account doesn't have an Xbox account");
}
else if (json.Properties["XErr"].StringValue == "2148916238")
else if (json["XErr"]!.GetStringValue() == "2148916238")
{
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
}
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
else throw new Exception("Unknown XSTS error code: " + json["XErr"]!.GetStringValue());
}
else
{
@ -426,9 +426,9 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
var json = Json.ParseJson(jsonString);
return json.Properties["access_token"].StringValue;
return json!["access_token"]!.GetStringValue();
}
/// <summary>
@ -448,8 +448,8 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0;
var json = Json.ParseJson(jsonString);
return json!["items"]!.AsArray().Count > 0;
}
public static UserProfile GetUserProfile(string accessToken)
@ -464,11 +464,11 @@ namespace MinecraftClient.Protocol
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
var json = Json.ParseJson(jsonString);
return new UserProfile()
{
UUID = json.Properties["id"].StringValue,
UserName = json.Properties["name"].StringValue
UUID = json!["id"]!.GetStringValue(),
UserName = json["name"]!.GetStringValue()
};
}

View file

@ -121,7 +121,7 @@ namespace MinecraftClient.Protocol
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/users/profiles/minecraft/" + name);
fetchTask.Wait();
string result = Json.ParseJson(fetchTask.Result).Properties["id"].StringValue;
string result = Json.ParseJson(fetchTask.Result)!["id"]!.GetStringValue();
fetchTask.Dispose();
return result;
}
@ -140,11 +140,11 @@ namespace MinecraftClient.Protocol
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/user/profiles/" + uuid + "/names");
fetchTask.Wait();
var nameChanges = Json.ParseJson(fetchTask.Result).DataArray;
var nameChanges = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
// Names are sorted from past to most recent. We need to get the last name in the list
return nameChanges[^1].Properties["name"].StringValue;
return nameChanges[^1]!["name"]!.GetStringValue();
}
catch (Exception) { return string.Empty; }
}
@ -157,40 +157,32 @@ namespace MinecraftClient.Protocol
public static Dictionary<string, DateTime> UuidToNameHistory(string uuid)
{
Dictionary<string, DateTime> tempDict = new();
List<Json.JSONData> jsonDataList;
System.Text.Json.Nodes.JsonArray jsonDataList;
// Perform web request
try
{
Task<string> fetchTask = httpClient.GetStringAsync("https://api.mojang.com/user/profiles/" + uuid + "/names");
fetchTask.Wait();
jsonDataList = Json.ParseJson(fetchTask.Result).DataArray;
jsonDataList = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
}
catch (Exception) { return tempDict; }
foreach (Json.JSONData jsonData in jsonDataList)
foreach (var jsonData in jsonDataList)
{
if (jsonData.Properties.Count > 1)
var obj = jsonData!.AsObject();
if (obj.Count > 1)
{
// Time is saved as long in the Unix format.
// Convert it to normal time, before adding it to the dictionary.
//
// !! FromUnixTimeMilliseconds does not exist in the current version. !!
// DateTimeOffset creationDate = DateTimeOffset.FromUnixTimeMilliseconds(Convert.ToInt64(jsonData.Properties["changedToAt"].StringValue));
//
DateTimeOffset creationDate = UnixTimeStampToDateTime(Convert.ToDouble(jsonData["changedToAt"].GetStringValue()));
// Workaround for converting Unix time to normal time.
DateTimeOffset creationDate = UnixTimeStampToDateTime(Convert.ToDouble(jsonData.Properties["changedToAt"].StringValue));
// Add Keyvaluepair to dict.
tempDict.Add(jsonData.Properties["name"].StringValue, creationDate.DateTime);
tempDict.Add(jsonData["name"]!.GetStringValue(), creationDate.DateTime);
}
// The first entry does not contain a change date.
else if (jsonData.Properties.Count > 0)
else if (obj.Count > 0)
{
// Add an undefined time to it.
tempDict.Add(jsonData.Properties["name"].StringValue, new DateTime());
tempDict.Add(jsonData["name"]!.GetStringValue(), new DateTime());
}
}
@ -203,14 +195,14 @@ namespace MinecraftClient.Protocol
/// <returns>Dictionary of the Mojang services</returns>
public static MojangServiceStatus GetMojangServiceStatus()
{
List<Json.JSONData> jsonDataList;
System.Text.Json.Nodes.JsonArray jsonDataList;
// Perform web request
try
{
Task<string> fetchTask = httpClient.GetStringAsync("https://status.mojang.com/check");
fetchTask.Wait();
jsonDataList = Json.ParseJson(fetchTask.Result).DataArray;
jsonDataList = Json.ParseJson(fetchTask.Result)!.AsArray();
fetchTask.Dispose();
}
catch (Exception)
@ -219,14 +211,14 @@ namespace MinecraftClient.Protocol
}
// 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)
return new MojangServiceStatus(minecraftNet: StringToServiceStatus(jsonDataList[0]!["minecraft.net"]!.GetStringValue()),
sessionMinecraftNet: StringToServiceStatus(jsonDataList[1]!["session.minecraft.net"]!.GetStringValue()),
accountMojangCom: StringToServiceStatus(jsonDataList[2]!["account.mojang.com"]!.GetStringValue()),
authserverMojangCom: StringToServiceStatus(jsonDataList[3]!["authserver.mojang.com"]!.GetStringValue()),
sessionserverMojangCom: StringToServiceStatus(jsonDataList[4]!["sessionserver.mojang.com"]!.GetStringValue()),
apiMojangCom: StringToServiceStatus(jsonDataList[5]!["api.mojang.com"]!.GetStringValue()),
texturesMinecraftNet: StringToServiceStatus(jsonDataList[6]!["textures.minecraft.net"]!.GetStringValue()),
mojangCom: StringToServiceStatus(jsonDataList[7]!["mojang.com"]!.GetStringValue())
);
}
@ -237,9 +229,9 @@ namespace MinecraftClient.Protocol
/// <returns>Dictionary with a link to the skin and cape of a player.</returns>
public static SkinInfo GetSkinInfo(string uuid)
{
Dictionary<string, Json.JSONData> textureDict;
System.Text.Json.Nodes.JsonObject textureObj;
string base64SkinInfo;
Json.JSONData decodedJsonSkinInfo;
System.Text.Json.Nodes.JsonNode? decodedJsonSkinInfo;
// Perform web request
try
@ -247,7 +239,7 @@ namespace MinecraftClient.Protocol
Task<string> fetchTask = httpClient.GetStringAsync("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
fetchTask.Wait();
// Obtain the Base64 encoded skin information from the API. Discard the rest, since it can be obtained easier through other requests.
base64SkinInfo = Json.ParseJson(fetchTask.Result).Properties["properties"].DataArray[0].Properties["value"].StringValue;
base64SkinInfo = Json.ParseJson(fetchTask.Result)!["properties"]![0]!["value"]!.GetStringValue();
fetchTask.Dispose();
}
catch (Exception) { return new SkinInfo(); }
@ -257,23 +249,18 @@ namespace MinecraftClient.Protocol
// Assert temporary variable for readablity.
// Contains skin and cape information.
textureDict = decodedJsonSkinInfo.Properties["textures"].Properties;
textureObj = decodedJsonSkinInfo!["textures"]!.AsObject();
// 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"))
if (textureObj.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");
return new SkinInfo(skinUrl: textureObj["SKIN"]!["url"] is not null ? textureObj["SKIN"]!["url"]!.GetStringValue() : string.Empty,
capeUrl: textureObj.ContainsKey("CAPE") ? textureObj["CAPE"]!["url"]!.GetStringValue() : string.Empty,
skinModel: textureObj["SKIN"]!["metadata"] is not null ? "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,
return new SkinInfo(capeUrl: textureObj.ContainsKey("CAPE") ? textureObj["CAPE"]!["url"]!.GetStringValue() : string.Empty,
skinModel: DefaultModelAlex(uuid) ? "Alex" : "Steve");
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Message;
using static MinecraftClient.Protocol.Message.LastSeenMessageList;
@ -37,17 +38,17 @@ namespace MinecraftClient.Protocol.ProfileKey
// see https://github.com/yushijinhun/authlib-injector/blob/da910956eaa30d2f6c2c457222d188aeb53b0d1f/src/main/java/moe/yushi/authlibinjector/httpd/ProfileKeyFilter.java#L49
// POST to "https://api.minecraftservices.com/player/certificates" with authlib-injector will get a dummy response
Json.JSONData json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body);
var json = isYggdrasil ? MakeDummyResponse() : Json.ParseJson(response!.Body);
// Error here
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
sig: json.Properties["publicKeySignature"].StringValue,
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
PublicKey publicKey = new(pemKey: json!["keyPair"]!["publicKey"]!.GetStringValue(),
sig: json["publicKeySignature"]!.GetStringValue(),
sigV2: json["publicKeySignatureV2"]!.GetStringValue());
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
PrivateKey privateKey = new(pemKey: json["keyPair"]!["privateKey"]!.GetStringValue());
return new PlayerKeyPair(publicKey, privateKey,
expiresAt: json.Properties["expiresAt"].StringValue,
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
expiresAt: json["expiresAt"]!.GetStringValue(),
refreshedAfter: json["refreshedAfter"]!.GetStringValue());
}
catch (Exception e)
{
@ -191,51 +192,10 @@ namespace MinecraftClient.Protocol.ProfileKey
return data.ToArray();
}
// https://github.com/mono/mono/blob/master/mcs/class/System.Json/System.Json/JsonValue.cs
public static string EscapeString(string src)
{
StringBuilder sb = new();
// Delegate to the shared Json.EscapeString backed by System.Text.Json
public static string EscapeString(string src) => Json.EscapeString(src);
int start = 0;
for (int i = 0; i < src.Length; i++)
{
char c = src[i];
bool needEscape = c < 32 || c == '"' || c == '\\';
// Broken lead surrogate
needEscape = needEscape || c >= '\uD800' && c <= '\uDBFF' &&
(i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF');
// Broken tail surrogate
needEscape = needEscape || c >= '\uDC00' && c <= '\uDFFF' &&
(i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF');
// To produce valid JavaScript
needEscape = needEscape || c == '\u2028' || c == '\u2029';
if (needEscape)
{
sb.Append(src, start, i - start);
switch (src[i])
{
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
case '\"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
default:
sb.Append("\\u");
sb.Append(((int)src[i]).ToString("x04"));
break;
}
start = i + 1;
}
}
sb.Append(src, start, src.Length - start);
return sb.ToString();
}
public static Json.JSONData MakeDummyResponse()
public static JsonNode MakeDummyResponse()
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048);
var mimePublicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());
@ -245,19 +205,20 @@ namespace MinecraftClient.Protocol.ProfileKey
DateTime now = DateTime.UtcNow;
DateTime expiresAt = now.AddHours(48);
DateTime refreshedAfter = now.AddHours(36);
Json.JSONData response = new(Json.JSONData.DataType.Object);
Json.JSONData keyPairObj = new(Json.JSONData.DataType.Object);
keyPairObj.Properties["privateKey"] = new(Json.JSONData.DataType.String){ StringValue = privateKeyPEM };
keyPairObj.Properties["publicKey"] = new(Json.JSONData.DataType.String){ StringValue = publicKeyPEM };
response.Properties["keyPair"] = keyPairObj;
response.Properties["publicKeySignature"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" };
response.Properties["publicKeySignatureV2"] = new(Json.JSONData.DataType.String){ StringValue = "AA==" };
string format = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
response.Properties["expiresAt"] = new(Json.JSONData.DataType.String){ StringValue = expiresAt.ToString(format) };
response.Properties["refreshedAfter"] = new(Json.JSONData.DataType.String){ StringValue = refreshedAfter.ToString(format) };
return response;
return new JsonObject
{
["keyPair"] = new JsonObject
{
["privateKey"] = privateKeyPEM,
["publicKey"] = publicKeyPEM
},
["publicKeySignature"] = "AA==",
["publicKeySignatureV2"] = "AA==",
["expiresAt"] = expiresAt.ToString(format),
["refreshedAfter"] = refreshedAfter.ToString(format)
};
}
}
}

View file

@ -581,16 +581,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -667,27 +666,25 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
if (loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
session.ID = loginResponse["accessToken"]!.GetStringValue();
if (loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"]
.StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.PlayerID = loginResponse["selectedProfile"]!["id"]!
.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else
{
string availableProfiles = "";
foreach (Json.JSONData profile in loginResponse.Properties["availableProfiles"]
.DataArray)
foreach (var profile in loginResponse["availableProfiles"]!.AsArray())
{
availableProfiles += " " + profile.Properties["name"].StringValue;
availableProfiles += " " + profile!["name"]!.GetStringValue();
}
ConsoleIO.WriteLine(Translations.mcc_avaliable_profiles + availableProfiles);
@ -703,19 +700,18 @@ namespace MinecraftClient.Protocol
ConsoleIO.WriteLine(Translations.mcc_selected_profile + " " + selectedProfileName);
Json.JSONData? selectedProfile = null;
foreach (Json.JSONData profile in loginResponse.Properties["availableProfiles"]
.DataArray)
System.Text.Json.Nodes.JsonNode? selectedProfile = null;
foreach (var profile in loginResponse["availableProfiles"]!.AsArray())
{
selectedProfile = profile.Properties["name"].StringValue == selectedProfileName
selectedProfile = profile!["name"]!.GetStringValue() == selectedProfileName
? profile
: selectedProfile;
}
if (selectedProfile != null)
if (selectedProfile is not null)
{
session.PlayerID = selectedProfile.Properties["id"].StringValue;
session.PlayerName = selectedProfile.Properties["name"].StringValue;
session.PlayerID = selectedProfile["id"]!.GetStringValue();
session.PlayerName = selectedProfile["name"]!.GetStringValue();
SessionToken currentsession = session;
return GetNewYggdrasilToken(currentsession, out session);
}
@ -889,7 +885,7 @@ namespace MinecraftClient.Protocol
{
var payload = JwtPayloadDecode.GetPayload(session.ID);
var json = Json.ParseJson(payload);
var expTimestamp = long.Parse(json.Properties["exp"].StringValue, NumberStyles.Any,
var expTimestamp = long.Parse(json!["exp"]!.GetStringValue(), NumberStyles.Any,
CultureInfo.CurrentCulture);
var now = DateTime.Now;
var tokenExp = UnixTimeStampToDateTime(expTimestamp);
@ -935,16 +931,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -986,16 +981,15 @@ namespace MinecraftClient.Protocol
}
else
{
Json.JSONData loginResponse = Json.ParseJson(result);
if (loginResponse.Properties.ContainsKey("accessToken")
&& loginResponse.Properties.ContainsKey("selectedProfile")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("id")
&& loginResponse.Properties["selectedProfile"].Properties.ContainsKey("name"))
var loginResponse = Json.ParseJson(result);
if (loginResponse?["accessToken"] is not null
&& loginResponse["selectedProfile"]?["id"] is not null
&& loginResponse["selectedProfile"]?["name"] is not null)
{
session.ID = loginResponse.Properties["accessToken"].StringValue;
session.PlayerID = loginResponse.Properties["selectedProfile"].Properties["id"].StringValue;
session.PlayerName = loginResponse.Properties["selectedProfile"].Properties["name"]
.StringValue;
session.ID = loginResponse["accessToken"]!.GetStringValue();
session.PlayerID = loginResponse["selectedProfile"]!["id"]!.GetStringValue();
session.PlayerName = loginResponse["selectedProfile"]!["name"]!
.GetStringValue();
return LoginResult.Success;
}
else return LoginResult.InvalidResponse;
@ -1065,28 +1059,27 @@ namespace MinecraftClient.Protocol
string cookies = String.Format("sid=token:{0}:{1};user={2};version={3}", accesstoken, uuid, username,
Program.MCHighestVersion);
DoHTTPSGet("pc.realms.minecraft.net", 443, "/worlds", cookies, ref result);
Json.JSONData realmsWorlds = Json.ParseJson(result);
if (realmsWorlds.Properties.ContainsKey("servers")
&& realmsWorlds.Properties["servers"].Type == Json.JSONData.DataType.Array
&& realmsWorlds.Properties["servers"].DataArray.Count > 0)
var realmsWorlds = Json.ParseJson(result);
if (realmsWorlds?["servers"] is System.Text.Json.Nodes.JsonArray serversArray
&& serversArray.Count > 0)
{
List<string> availableWorlds = new(); // Store string to print
int index = 0;
foreach (Json.JSONData realmsServer in realmsWorlds.Properties["servers"].DataArray)
foreach (var realmsServer in serversArray)
{
if (realmsServer.Properties.ContainsKey("name")
&& realmsServer.Properties.ContainsKey("owner")
&& realmsServer.Properties.ContainsKey("id")
&& realmsServer.Properties.ContainsKey("expired"))
if (realmsServer?["name"] is not null
&& realmsServer["owner"] is not null
&& realmsServer["id"] is not null
&& realmsServer["expired"] is not null)
{
if (realmsServer.Properties["expired"].StringValue == "false")
if (realmsServer["expired"].GetStringValue() == "false")
{
availableWorlds.Add(String.Format("[{0}] {2} ({3}) - {1}",
index++,
realmsServer.Properties["id"].StringValue,
realmsServer.Properties["name"].StringValue,
realmsServer.Properties["owner"].StringValue));
realmsWorldsResult.Add(realmsServer.Properties["id"].StringValue);
realmsServer["id"]!.GetStringValue(),
realmsServer["name"]!.GetStringValue(),
realmsServer["owner"]!.GetStringValue()));
realmsWorldsResult.Add(realmsServer["id"]!.GetStringValue());
}
}
}
@ -1132,9 +1125,9 @@ namespace MinecraftClient.Protocol
cookies, ref result);
if (statusCode == 200)
{
Json.JSONData serverAddress = Json.ParseJson(result);
if (serverAddress.Properties.ContainsKey("address"))
return serverAddress.Properties["address"].StringValue;
var serverAddress = Json.ParseJson(result);
if (serverAddress?["address"] is not null)
return serverAddress["address"]!.GetStringValue();
else
{
ConsoleIO.WriteLine(Translations.error_realms_ip_error);

View file

@ -1,413 +1,206 @@
using System;
using System.Collections.Generic;
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using MinecraftClient.Proxy;
namespace MinecraftClient.Protocol
{
/// <summary>
/// Create a new http request and optionally with proxy according to setting
/// HTTP client with automatic proxy support based on application settings.
/// Backed by System.Net.Http.HttpClient with SocketsHttpHandler.
/// </summary>
public class ProxiedWebRequest
{
public interface ITcpFactory
private const int DefaultConnectTimeoutSeconds = 30;
private readonly Uri _uri;
public NameValueCollection Headers { get; } = new();
public string UserAgent
{
TcpClient CreateTcpClient(string host, int port);
};
get => Headers.Get("User-Agent") ?? string.Empty;
set => Headers.Set("User-Agent", value);
}
private readonly string httpVersion = "HTTP/1.1";
public string Accept
{
get => Headers.Get("Accept") ?? string.Empty;
set => Headers.Set("Accept", value);
}
private ITcpFactory? tcpFactory;
private bool isProxied = false; // Send absolute Url in request if true
public string Cookie
{
set => Headers.Set("Cookie", value);
}
private readonly Uri uri;
private string Host { get { return uri.Host; } }
private int Port { get { return uri.Port; } }
private string Path { get { return uri.PathAndQuery; } }
private string AbsoluteUrl { get { return uri.AbsoluteUri; } }
private bool IsSecure { get { return uri.Scheme == "https"; } }
public NameValueCollection Headers = new();
public string UserAgent { get { return Headers.Get("User-Agent") ?? String.Empty; } set { Headers.Set("User-Agent", value); } }
public string Accept { get { return Headers.Get("Accept") ?? String.Empty; } set { Headers.Set("Accept", value); } }
public string Cookie { set { Headers.Set("Cookie", value); } }
public bool Debug => Settings.Config.Logging.DebugMessages;
/// <summary>
/// Set to true to tell the http client proxy is enabled
/// </summary>
public bool IsProxy { get { return isProxied; } set { isProxied = value; } }
public bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
/// <summary>
/// Create a new http request
/// Create a new HTTP request
/// </summary>
/// <param name="url">Target URL</param>
public ProxiedWebRequest(string url)
{
uri = new Uri(url);
_uri = new Uri(url);
SetupBasicHeaders();
}
/// <summary>
/// Create a new http request with cookies
/// Create a new HTTP request with cookies
/// </summary>
/// <param name="url">Target URL</param>
/// <param name="cookies">Cookies to use</param>
/// <param name="cookies">Cookies to include in the request</param>
public ProxiedWebRequest(string url, NameValueCollection cookies)
{
uri = new Uri(url);
_uri = new Uri(url);
Headers.Add("Cookie", GetCookieString(cookies));
SetupBasicHeaders();
}
/// <summary>
/// Create a new http request with custom tcp client
/// </summary>
/// <param name="tcpFactory">Tcp factory to be used</param>
/// <param name="url">Target URL</param>
public ProxiedWebRequest(ITcpFactory tcpFactory, string url) : this(url)
{
this.tcpFactory = tcpFactory;
}
/// <summary>
/// Create a new http request with custom tcp client and cookies
/// </summary>
/// <param name="tcpFactory">Tcp factory to be used</param>
/// <param name="url">Target URL</param>
/// <param name="cookies">Cookies to use</param>
public ProxiedWebRequest(ITcpFactory tcpFactory, string url, NameValueCollection cookies) : this(url, cookies)
{
this.tcpFactory = tcpFactory;
}
/// <summary>
/// Setup some basic headers
/// </summary>
private void SetupBasicHeaders()
{
Headers.Add("Host", Host);
Headers.Add("Host", _uri.Host);
Headers.Add("User-Agent", "MCC/1.0");
Headers.Add("Accept", "*/*");
Headers.Add("Connection", "close");
}
/// <summary>
/// Perform GET request and get the response. Proxy is handled automatically
/// Perform GET request. Proxy is handled automatically.
/// </summary>
/// <returns></returns>
public Response Get()
{
return Send("GET");
}
public Response Get() => Send(HttpMethod.Get);
/// <summary>
/// Perform POST request and get the response. Proxy is handled automatically
/// Perform POST request. Proxy is handled automatically.
/// </summary>
/// <param name="contentType">The content type of request body</param>
/// <param name="body">Request body</param>
/// <returns></returns>
public Response Post(string contentType, string body)
{
Headers.Add("Content-Type", contentType);
// Calculate length
Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString());
return Send("POST", body);
}
public Response Post(string contentType, string body) => Send(HttpMethod.Post, contentType, body);
/// <summary>
/// Send a http request to the server. Proxy is handled automatically
/// Send an HTTP request. Proxy is configured automatically from Settings.
/// </summary>
/// <param name="method">Method in string representation</param>
/// <param name="body">Optional request body</param>
/// <returns></returns>
private Response Send(string method, string body = "")
private Response Send(HttpMethod method, string? contentType = null, string? body = null)
{
List<string> requestMessage = new()
using var handler = CreateHandler();
using var client = new HttpClient(handler);
using var request = new HttpRequestMessage(method, _uri);
// Apply custom headers (skip content-level headers)
foreach (string key in Headers)
{
string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line
};
foreach (string key in Headers) // Headers
{
var value = Headers[key];
requestMessage.Add(string.Format("{0}: {1}", key, value));
if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Host", StringComparison.OrdinalIgnoreCase))
continue;
request.Headers.TryAddWithoutValidation(key, Headers[key]);
}
requestMessage.Add(""); // <CR><LF>
if (body != "")
if (body is not null)
{
requestMessage.Add(body);
request.Content = new StringContent(body, Encoding.UTF8, contentType ?? "text/plain");
}
else requestMessage.Add(""); // <CR><LF>
if (Debug)
{
foreach (string l in requestMessage)
{
ConsoleIO.WriteLine("< " + l);
}
ConsoleIO.WriteLine($"< {method} {_uri}");
foreach (string key in Headers)
ConsoleIO.WriteLine($"< {key}: {Headers[key]}");
}
Response response = Response.Empty();
// FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class
// TcpClient client = tcpFactory.CreateTcpClient(Host, Port);
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true);
Stream stream;
if (IsSecure)
{
stream = new SslStream(client.GetStream());
((SslStream)stream).AuthenticateAsClient(Host, null, SslProtocols.Tls12, true); // Enable TLS 1.2. Hotfix for #1774
}
else
{
stream = client.GetStream();
}
string h = string.Join("\r\n", requestMessage.ToArray());
byte[] data = Encoding.ASCII.GetBytes(h);
stream.Write(data, 0, data.Length);
stream.Flush();
// Read response
int statusCode = ReadHttpStatus(stream);
var headers = ReadHeader(stream);
string? rbody;
if (headers.Get("transfer-encoding") == "chunked")
{
rbody = ReadBodyChunked(stream);
}
else
{
rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
}
if (headers.Get("set-cookie") != null)
{
response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty<string>());
}
response.Body = rbody ?? "";
response.StatusCode = statusCode;
response.Headers = headers;
try
{
stream.Close();
client.Close();
}
catch { }
using var httpResponse = client.Send(request);
using var stream = httpResponse.Content.ReadAsStream();
using var reader = new System.IO.StreamReader(stream);
string responseBody = reader.ReadToEnd();
return response;
}
var responseHeaders = new NameValueCollection();
foreach (var header in httpResponse.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
foreach (var header in httpResponse.Content.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
/// <summary>
/// Read HTTP response line from a Stream
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns></returns>
/// <exception cref="InvalidDataException">If server return unknown data</exception>
private static int ReadHttpStatus(Stream s)
{
var httpHeader = ReadLine(s); // http header line
if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0"))
{
return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture);
}
else
{
throw new InvalidDataException("Unexpect data from server");
}
}
/// <summary>
/// Read HTTP headers from a Stream
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Headers in lower-case</returns>
private static NameValueCollection ReadHeader(Stream s)
{
var headers = new NameValueCollection();
// Read headers
string header;
do
{
header = ReadLine(s);
if (!String.IsNullOrEmpty(header))
var cookies = new NameValueCollection();
foreach (System.Net.Cookie cookie in handler.CookieContainer.GetCookies(_uri))
{
var tmp = header.Split(new char[] { ':' }, 2);
var name = tmp[0].ToLower();
var value = tmp[1].Trim();
headers.Add(name, value);
if (!cookie.Expired)
cookies.Add(cookie.Name, cookie.Value);
}
return new Response((int)httpResponse.StatusCode, responseBody, responseHeaders, cookies);
}
catch (HttpRequestException ex)
{
if (Debug)
ConsoleIO.WriteLine("HTTP error: " + ex.Message);
return Response.Empty();
}
while (!String.IsNullOrEmpty(header));
return headers;
}
/// <summary>
/// Read HTTP body from a Stream
/// Create a SocketsHttpHandler with proxy support from ProxyHandler settings.
/// </summary>
/// <param name="s">Stream to read</param>
/// <param name="length">Length of the body (the Content-Length header)</param>
/// <returns>Body or null if length is zero</returns>
private static string? ReadBody(Stream s, int length)
private static SocketsHttpHandler CreateHandler()
{
if (length > 0)
var handler = new SocketsHttpHandler
{
byte[] buffer = new byte[length];
int r = 0;
while (r < length)
UseCookies = true,
CookieContainer = new CookieContainer(),
AllowAutoRedirect = false,
ConnectTimeout = TimeSpan.FromSeconds(DefaultConnectTimeoutSeconds),
};
if (ProxyHandler.Config.Enabled_Login)
{
string proxyScheme = ProxyHandler.Config.Proxy_Type switch
{
var read = s.Read(buffer, r, length - r);
r += read;
Thread.Sleep(50);
ProxyHandler.Configs.ProxyType.SOCKS4 => "socks4",
ProxyHandler.Configs.ProxyType.SOCKS4a => "socks4a",
ProxyHandler.Configs.ProxyType.SOCKS5 => "socks5",
_ => "http"
};
var proxyUri = new Uri($"{proxyScheme}://{ProxyHandler.Config.Server.Host}:{ProxyHandler.Config.Server.Port}");
var proxy = new WebProxy(proxyUri);
if (!string.IsNullOrWhiteSpace(ProxyHandler.Config.Username) &&
!string.IsNullOrWhiteSpace(ProxyHandler.Config.Password))
{
proxy.Credentials = new NetworkCredential(
ProxyHandler.Config.Username,
ProxyHandler.Config.Password);
}
return Encoding.UTF8.GetString(buffer);
}
else
{
return null;
handler.Proxy = proxy;
handler.UseProxy = true;
}
return handler;
}
/// <summary>
/// Read HTTP chunked body from a Stream
/// Build a cookie header value from a NameValueCollection.
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Body or empty string if nothing is received</returns>
private static string ReadBodyChunked(Stream s)
{
List<byte> buffer1 = new();
while (true)
{
string l = ReadLine(s);
int size = Int32.Parse(l, NumberStyles.HexNumber);
if (size == 0)
break;
byte[] buffer2 = new byte[size];
int r = 0;
while (r < size)
{
var read = s.Read(buffer2, r, size - r);
r += read;
Thread.Sleep(50);
}
ReadLine(s);
buffer1.AddRange(buffer2);
}
return Encoding.UTF8.GetString(buffer1.ToArray());
}
/// <summary>
/// Parse the Set-Cookie header value into NameValueCollection. Cookie options are ignored
/// </summary>
/// <param name="headerValue">Array of value strings</param>
/// <returns>Parsed cookies</returns>
private static NameValueCollection ParseSetCookie(IEnumerable<string> headerValue)
{
NameValueCollection cookies = new();
foreach (var value in headerValue)
{
string[] cookie = value.Split(';'); // cookie options are ignored
string[] tmp = cookie[0].Split(new char[] { '=' }, 2); // Split first '=' only
string[] options = cookie[1..];
string cname = tmp[0].Trim();
string cvalue = tmp[1].Trim();
// Check expire
bool isExpired = false;
foreach (var option in options)
{
var tmp2 = option.Trim().Split(new char[] { '=' }, 2);
// Check for Expires=<date> and Max-Age=<number>
if (tmp2.Length == 2)
{
var optName = tmp2[0].Trim().ToLower();
var optValue = tmp2[1].Trim();
switch (optName)
{
case "expires":
{
if (DateTime.TryParse(optValue, out var expDate))
{
if (expDate < DateTime.Now)
isExpired = true;
}
break;
}
case "max-age":
{
if (int.TryParse(optValue, out var expInt))
{
if (expInt <= 0)
isExpired = true;
}
break;
}
}
}
if (isExpired)
break;
}
if (!isExpired)
cookies.Add(cname, cvalue);
}
return cookies;
}
/// <summary>
/// Read a line from a Stream
/// </summary>
/// <remarks>
/// Line break by \r\n and they are not included in returned string
/// </remarks>
/// <param name="s">Stream to read</param>
/// <returns>String</returns>
private static string ReadLine(Stream s)
{
List<byte> buffer = new();
byte c;
while (true)
{
int b = s.ReadByte();
if (b == -1)
break;
c = (byte)b;
if (c == '\n')
{
if (buffer.Last() == '\r')
{
buffer.RemoveAt(buffer.Count - 1);
break;
}
}
buffer.Add(c);
}
return Encoding.UTF8.GetString(buffer.ToArray());
}
/// <summary>
/// Get the cookie string representation to use in header
/// </summary>
/// <param name="cookies"></param>
/// <returns></returns>
private static string GetCookieString(NameValueCollection cookies)
{
var sb = new StringBuilder();
foreach (string key in cookies)
{
var value = cookies[key];
sb.Append(string.Format("{0}={1}; ", key, value));
sb.Append($"{key}={cookies[key]}; ");
}
string result = sb.ToString();
return result.Remove(result.Length - 2); // Remove "; " at the end
return result.Length >= 2 ? result[..^2] : result;
}
/// <summary>
/// Basic response object
/// Basic HTTP response object.
/// </summary>
public class Response
{
@ -424,14 +217,8 @@ namespace MinecraftClient.Protocol
Cookies = cookies;
}
/// <summary>
/// Get an empty response object
/// </summary>
/// <returns></returns>
public static Response Empty()
{
return new Response(204 /* No content */, "", new NameValueCollection(), new NameValueCollection());
}
public static Response Empty() =>
new(204, "", new NameValueCollection(), new NameValueCollection());
public override string ToString()
{
@ -439,26 +226,18 @@ namespace MinecraftClient.Protocol
sb.AppendLine("Status code: " + StatusCode);
sb.AppendLine("Headers:");
foreach (string key in Headers)
{
sb.AppendLine(string.Format(" {0}: {1}", key, Headers[key]));
}
sb.AppendLine($" {key}: {Headers[key]}");
if (Cookies.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Cookies: ");
foreach (string key in Cookies)
{
sb.AppendLine(string.Format(" {0}={1}", key, Cookies[key]));
}
sb.AppendLine($" {key}={Cookies[key]}");
}
if (Body != "")
{
sb.AppendLine();
if (Body.Length > 200)
{
sb.AppendLine("Body: (Truncated to 200 characters)");
}
else sb.AppendLine("Body: ");
sb.AppendLine(Body.Length > 200 ? "Body: (Truncated to 200 characters)" : "Body: ");
sb.AppendLine(Body.Length > 200 ? Body[..200] + "..." : Body);
}
return sb.ToString();

View file

@ -123,35 +123,36 @@ namespace MinecraftClient.Protocol.Session
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
System.Text.Json.Nodes.JsonNode? mcSession = null;
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
if (mcSession is System.Text.Json.Nodes.JsonObject mcSessionObj
&& mcSessionObj.ContainsKey("clientToken")
&& mcSessionObj.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
string clientID = mcSession["clientToken"]!.GetStringValue().Replace("-", "");
var sessionItems = mcSession["authenticationDatabase"]!.AsObject();
foreach (var kvp in sessionItems)
{
string key = kvp.Key;
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
var sessionItem = kvp.Value!.AsObject();
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
string login = Settings.ToLowerIfNeed(sessionItem["username"]!.GetStringValue());
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
sessionItem["accessToken"]!.GetStringValue(),
sessionItem["displayName"]!.GetStringValue(),
sessionItem["uuid"]!.GetStringValue().Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)

View file

@ -146,7 +146,7 @@ namespace MinecraftClient.Scripting
/// Any text sent by the server will be sent here by MinecraftCom (extended variant)
/// </summary>
/// <remarks>
/// You can use Json.ParseJson() to process the JSON string.
/// You can use Json.ParseJson() to obtain a System.Text.Json.Nodes.JsonNode for processing.
/// </remarks>
/// <param name="text">Text from the server</param>
/// <param name="json">Raw JSON from the server. This parameter will be NULL on MC 1.5 or lower!</param>

View file

@ -143,7 +143,7 @@ class SkinAPI
var request = new ProxiedWebRequest("https://api.mojang.com/users/profiles/minecraft/" + name);
request.Accept = "application/json";
var response = request.Get();
string uuid = Json.ParseJson(response.Body).Properties["id"].StringValue;
string uuid = Json.ParseJson(response.Body)!["id"]!.GetStringValue();
settings.GetNamesToUuidMojangCache().Add(name, uuid);
return uuid;
}

View file

@ -426,9 +426,9 @@ internal class VkLongPoolClient
var jsonResult = CallVkMethod("groups.getLongPollServer", "group_id=" + BotCommunityId);
var data = Json.ParseJson(jsonResult);
Key = data.Properties["response"].Properties["key"].StringValue;
Server = data.Properties["response"].Properties["server"].StringValue;
LastTs = Convert.ToInt32(data.Properties["response"].Properties["ts"].StringValue);
Key = data!["response"]!["key"]!.GetStringValue();
Server = data["response"]!["server"]!.GetStringValue();
LastTs = Convert.ToInt32(data["response"]!["ts"].GetStringValue());
}
private void StartLongPoolAsync()
@ -457,25 +457,25 @@ internal class VkLongPoolClient
{
var j = JsonConvert.DeserializeObject(jsonData) as JObject;
var data = Json.ParseJson(jsonData);
if (data.Properties.ContainsKey("failed"))
if (data?.AsObject().ContainsKey("failed") == true)
{
Init();
}
LastTs = Convert.ToInt32(data.Properties["ts"].StringValue);
var updates = data.Properties["updates"].DataArray;
LastTs = Convert.ToInt32(data!["ts"].GetStringValue());
var updates = data["updates"]!.AsArray();
List<Tuple<string, string, string, string, string, string>> messages = new List<Tuple<string, string, string, string, string, string>>();
foreach (var str in updates)
{
if (str.Properties["type"].StringValue != "message_new") continue;
if (str!["type"]!.GetStringValue() != "message_new") continue;
var msgData = str.Properties["object"].Properties;
var msgData = str["object"]!.AsObject();
var id = msgData["from_id"].StringValue;
var userId = msgData["from_id"].StringValue;
var peer_id = msgData["peer_id"].StringValue;
var id = msgData["from_id"]!.GetStringValue();
var userId = msgData["from_id"]!.GetStringValue();
var peer_id = msgData["peer_id"]!.GetStringValue();
string event_id = "";
var msgText = msgData["text"].StringValue;
var conversation_message_id = msgData["conversation_message_id"].StringValue;
var msgText = msgData["text"]!.GetStringValue();
var conversation_message_id = msgData["conversation_message_id"]!.GetStringValue();
messages.Add(new Tuple<string, string, string, string, string, string>(userId, peer_id, msgText, conversation_message_id, id, event_id));
}