A lot more type definitions

This commit is contained in:
Kecskeméti László 2024-06-26 00:36:59 +02:00
parent a7a087b637
commit 2b18bd8ae4
22 changed files with 361 additions and 119 deletions

View File

@ -1,4 +1,7 @@
using System.Text.Json; using System.Text.Json;
using Newtonsoft.Json;
using Xunit.Abstractions;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Discord.API.Tests; namespace Discord.API.Tests;
using Xunit; using Xunit;
@ -6,26 +9,132 @@ using Discord.API;
public class JsonTests public class JsonTests
{ {
[Fact] private readonly ITestOutputHelper _testOutputHelper;
public void GatewayPacketEncodeJsonTest()
public JsonTests(ITestOutputHelper testOutputHelper)
{ {
var gateway_packet = new GatewayPacket() _testOutputHelper = testOutputHelper;
{
Op = GatewayPacket.Opcode.Heartbeat
};
string json = JsonSerializer.Serialize(gateway_packet, SourceGenerationContext.Default.GatewayPacket);
Assert.Equal("""{"op":1}""", json);
} }
[Fact] [Fact]
public void GatewayPacketDecodeTest() public void InvalidSessionDeserialize()
{ {
const string src = """{"op":1}"""; string src = """
var gateway_packet = {
JsonSerializer.Deserialize<GatewayPacket>(src, SourceGenerationContext.Default.GatewayPacket); "op": 9
Assert.Equal(GatewayPacket.Opcode.Heartbeat, gateway_packet?.Op); }
""";
GatewayPacket? gateway_packet = JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket);
Assert.IsType<InvalidSessionPacket>(gateway_packet);
}
[Fact]
public void ReconnectPacketDeserialize()
{
string src = """
{
"op": 7,
"d": null
}
""";
GatewayPacket? gateway_packet = JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket);
Assert.IsType<ReconnectPacket>(gateway_packet);
}
[Fact]
public void ResumePacketSerialize()
{
ResumePacket gateway_packet = new()
{
Data = new()
{
Token = "tokenlol",
Sequence = 10,
SessionId = "sessionlol69"
}
};
string serialized = JsonSerializer.Serialize(gateway_packet, SourceGenerationContext.Default.GatewayPacket);
_testOutputHelper.WriteLine($"Serilazed Resume packet: {serialized}");
//TODO: Verify the string
}
[Fact]
public void HelloPacketDeserialize()
{
string src = """
{
"op": 10,
"d": {
"heartbeat_interval": 45000
}
}
""";
GatewayPacket? gateway_packet = JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket);
Assert.IsType<HelloPacket>(gateway_packet);
Assert.True(gateway_packet is HelloPacket
{
Op: GatewayPacket.Opcode.Hello,
Data.HeartbeatInterval: 45000
});
}
[Fact]
public void HeartbeatAckDeserialize()
{
string src = """
{"op":11}
""";
GatewayPacket? gateway_packet = JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket);
Assert.IsType<HeartbeatAckPacket>(gateway_packet);
Assert.Equal(GatewayPacket.Opcode.HeartbeatAck, gateway_packet.Op);
}
[Fact]
public void HeartbeatPacketSerialize()
{
HeartbeatPacket heartbeat_packet = new()
{
Sequence = 69
};
string serialized = JsonSerializer.Serialize(heartbeat_packet, SourceGenerationContext.Default.GatewayPacket);
JsonDocument json_doc = JsonDocument.Parse(serialized);
Assert.True(json_doc.RootElement.TryGetProperty("op", out var opcode) &&
opcode.ValueKind == JsonValueKind.Number &&
opcode.TryGetInt32(out int op) && op == (int)GatewayPacket.Opcode.Heartbeat);
Assert.True(json_doc.RootElement.TryGetProperty("d", out var sequence) &&
sequence.ValueKind == JsonValueKind.Number &&
sequence.TryGetUInt64(out ulong seq) && seq == 69);
}
[Fact]
public void IdentifyPacketSerialize()
{
IdentifyPacket identify_packet = new()
{
Data = new()
{
Token = "token_lol",
Intents = 6969,
Shard = [1, 2],
LargeThreshold = 42
}
};
string serialized =
JsonSerializer.Serialize(identify_packet, SourceGenerationContext.Default.GatewayPacket);
_testOutputHelper.WriteLine($"Serialized Identify packet: {serialized}");
//TODO: Test the output string
} }
[Fact] [Fact]
@ -146,4 +255,53 @@ public class JsonTests
}); });
} }
[Fact]
public void ChannelUpdateDeserialize()
{
string src = """
{
"op":0,
"t":"CHANNEL_UPDATE",
"s":1,
"d":{
"id": "922243411795390570",
"type": 2,
"guild_id": "5678",
"position": 3,
"name": "voice csennel",
"topic": "A very interesting topic",
"nsfw": true,
"last_message_id":"6969",
"bitrate":420,
"parent_id": "5555"
}
}
""";
var gateway_packet =
JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket);
Assert.IsType<ChannelUpdatePacket>(gateway_packet);
Assert.True(gateway_packet is ChannelUpdatePacket
{
Op: GatewayPacket.Opcode.Dispatch,
Event: "CHANNEL_UPDATE",
Sequence: 1,
Data:
{
Id: 922243411795390570,
Name: "voice csennel",
GuildId: 5678,
Type: 2,
Position: 3,
Topic: "A very interesting topic",
Nsfw: true,
Bitrate: 420,
ParentId: 5555,
LastMessageId: 6969
}
});
}
} }

View File

@ -4,17 +4,15 @@ namespace Discord.API;
public class ChannelData public class ChannelData
{ {
[JsonRequired] public required ulong Id { get; init; }
public ulong Id; public int? Type { get; init; }
[JsonRequired] public ulong? GuildId { get; init; }
public int Type; public int? Position { get; init; }
public ulong? GuildId; public string? Name { get; init; }
public int? Position; public string? Topic { get; init; }
public string? Name; public bool? Nsfw { get; init; }
public string? Topic; public ulong? LastMessageId { get; init; }
public bool? Nsfw; public int? Bitrate { get; init; }
public ulong? LastMessageId; public ulong? ParentId { get; init; }
public int? Bitrate;
public ulong? ParentId;
//TODO: Missing fields //TODO: Missing fields
} }

View File

@ -4,9 +4,6 @@ namespace Discord.API;
public sealed class PartialApplicationData public sealed class PartialApplicationData
{ {
[JsonRequired] public required ulong Id { get; init; }
public ulong Id; public ulong Flags { get; init; }
[JsonRequired]
public ulong Flags;
} }

View File

@ -4,8 +4,6 @@ namespace Discord.API;
public sealed class UnavailableGuildData public sealed class UnavailableGuildData
{ {
[JsonRequired] public required ulong Id { get; init; }
public ulong Id; public bool Unavailable { get; init; }
[JsonRequired]
public bool Unavailable;
} }

View File

@ -5,12 +5,9 @@ namespace Discord.API;
public sealed class UserData public sealed class UserData
{ {
[JsonRequired] public required ulong Id { get; init; }
public ulong Id; public string? Username { get; init; }
[JsonRequired] public string? Discriminator { get; init; }
public string Username; public string? GlobalName { get; init; }
[JsonRequired]
public string Discriminator;
public string? GlobalName;
//TODO More fields //TODO More fields
} }

View File

@ -5,7 +5,9 @@ namespace Discord.API;
internal class ChannelCreatePacket : DispatchPacket internal class ChannelCreatePacket : DispatchPacket
{ {
public override string Event => "CHANNEL_CREATE";
[JsonRequired] [JsonRequired]
[JsonPropertyName("d")] [JsonPropertyName("d")]
public ChannelData Data; public required ChannelData Data { get; init; }
} }

View File

@ -5,7 +5,9 @@ namespace Discord.API;
internal class ChannelDeletePacket : DispatchPacket internal class ChannelDeletePacket : DispatchPacket
{ {
public override string Event => "CHANNEL_DELETE";
[JsonRequired] [JsonRequired]
[JsonPropertyName("d")] [JsonPropertyName("d")]
public ChannelData Data; public required ChannelData Data { get; init; }
} }

View File

@ -5,7 +5,9 @@ namespace Discord.API;
internal class ChannelUpdatePacket : DispatchPacket internal class ChannelUpdatePacket : DispatchPacket
{ {
public override string Event => "CHANNEL_UPDATE";
[JsonRequired] [JsonRequired]
[JsonPropertyName("d")] [JsonPropertyName("d")]
public ChannelData Data; public required ChannelData Data { get; init; }
} }

View File

@ -3,16 +3,17 @@ using System.Text.Json.Serialization;
namespace Discord.API; namespace Discord.API;
[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "t", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(ChannelCreatePacket))] [JsonDerivedType(typeof(ChannelCreatePacket))]
[JsonDerivedType(typeof(ChannelUpdatePacket))] [JsonDerivedType(typeof(ChannelUpdatePacket))]
[JsonDerivedType(typeof(ChannelDeletePacket))] [JsonDerivedType(typeof(ChannelDeletePacket))]
[JsonDerivedType(typeof(ReadyPacket))] [JsonDerivedType(typeof(ReadyPacket))]
internal class DispatchPacket : GatewayPacket internal abstract class DispatchPacket : GatewayPacket
{ {
public override Opcode Op => Opcode.Dispatch;
[JsonPropertyName("t")] [JsonPropertyName("t")]
[JsonRequired] public abstract string Event { get; }
public string Event;
[JsonPropertyName("s")] [JsonPropertyName("s")]
public ulong? Sequence; public ulong? Sequence;

View File

@ -1,5 +1,4 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
namespace Discord.API; namespace Discord.API;
@ -7,28 +6,30 @@ namespace Discord.API;
[JsonDerivedType(typeof(ReadyPacket))] [JsonDerivedType(typeof(ReadyPacket))]
internal class ReadyPacket : DispatchPacket internal class ReadyPacket : DispatchPacket
{ {
public sealed class ReadyData public override string Event => "READY";
public struct ReadyData
{ {
[JsonRequired]
[JsonPropertyName("v")] [JsonPropertyName("v")]
public int Version; public required int Version { get; init; }
[JsonRequired] [JsonRequired]
public UserData User; public required UserData User { get; init; }
[JsonRequired] [JsonRequired]
public UnavailableGuildData[] Guilds; public required UnavailableGuildData[] Guilds { get; init; }
[JsonRequired] [JsonRequired]
public string SessionId; public required string SessionId { get; init; }
[JsonRequired] [JsonRequired]
public string ResumeGatewayUrl; public required string ResumeGatewayUrl { get; init; }
[JsonRequired] [JsonRequired]
public PartialApplicationData Application; public required PartialApplicationData Application { get; init; }
} }
[JsonRequired] [JsonRequired]
[JsonPropertyName("d")] [JsonPropertyName("d")]
public ReadyData Data; public required ReadyData Data { get; init; }
} }

View File

@ -3,10 +3,16 @@ using System.Text.Json.Serialization;
namespace Discord.API; namespace Discord.API;
[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "op", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(IdentifyPacket))] [JsonDerivedType(typeof(IdentifyPacket))]
[JsonDerivedType(typeof(DispatchPacket))] [JsonDerivedType(typeof(DispatchPacket))]
internal class GatewayPacket [JsonDerivedType(typeof(HeartbeatPacket))]
[JsonDerivedType(typeof(HeartbeatAckPacket))]
[JsonDerivedType(typeof(HelloPacket))]
[JsonDerivedType(typeof(ResumePacket))]
[JsonDerivedType(typeof(ReconnectPacket))]
[JsonDerivedType(typeof(InvalidSessionPacket))]
public abstract class GatewayPacket
{ {
public enum Opcode public enum Opcode
{ {
@ -23,7 +29,6 @@ internal class GatewayPacket
HeartbeatAck = 11 HeartbeatAck = 11
} }
[JsonRequired]
[JsonPropertyName("op")] [JsonPropertyName("op")]
public Opcode Op; public abstract Opcode Op { get; }
} }

View File

@ -1,3 +1,4 @@
using System.Reflection.Emit;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -8,53 +9,54 @@ internal class GatewayPacketConverter : JsonConverter<GatewayPacket>
public override GatewayPacket? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override GatewayPacket? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
JsonDocument json_doc = JsonDocument.ParseValue(ref reader); JsonDocument json_doc = JsonDocument.ParseValue(ref reader);
if(json_doc.RootElement.TryGetProperty("op", out JsonElement opcode_element)) if (!json_doc.RootElement.TryGetProperty("op", out JsonElement opcode_element))
{
if (opcode_element.ValueKind != JsonValueKind.Number)
{
throw new JsonException("Opcode is not a number");
}
GatewayPacket.Opcode op = (GatewayPacket.Opcode)opcode_element.GetInt32();
switch (op)
{
case GatewayPacket.Opcode.Dispatch:
if (json_doc.RootElement.TryGetProperty("t", out JsonElement event_element))
{
if (event_element.ValueKind != JsonValueKind.String)
{
throw new Exception("Event name not string");
}
string? event_name = event_element.GetString();
switch (event_name)
{
case "READY":
return json_doc.Deserialize(SourceGenerationContext.Default.ReadyPacket);
case "CHANNEL_CREATE":
return json_doc.Deserialize(SourceGenerationContext.Default
.ChannelCreatePacket);
case "CHANNEL_UPDATE":
return json_doc.Deserialize(SourceGenerationContext.Default.ChannelUpdatePacket);
case "CHANNEL_DELETE":
return json_doc.Deserialize(SourceGenerationContext.Default.ChannelDeletePacket);
default:
throw new NotSupportedException($"Packet {event_name} is not supported");
}
}
else
{
throw new JsonException("Event name not found");
}
default:
throw new NotSupportedException($"Opcode {op} is not supported in json deserialization");
}
}
else
{ {
throw new Exception("Opcode not found"); throw new Exception("Opcode not found");
} }
if (opcode_element.ValueKind != JsonValueKind.Number)
{
throw new JsonException("Opcode is not a number");
}
GatewayPacket.Opcode op = (GatewayPacket.Opcode)opcode_element.GetInt32();
switch (op)
{
case GatewayPacket.Opcode.HeartbeatAck:
return json_doc.Deserialize(SourceGenerationContext.Default.HeartbeatAckPacket);
case GatewayPacket.Opcode.Hello:
return json_doc.Deserialize(SourceGenerationContext.Default.HelloPacket);
case GatewayPacket.Opcode.Reconnect:
return json_doc.Deserialize(SourceGenerationContext.Default.ReconnectPacket);
case GatewayPacket.Opcode.InvalidSession:
return json_doc.Deserialize(SourceGenerationContext.Default.InvalidSessionPacket);
case GatewayPacket.Opcode.Dispatch:
if (!json_doc.RootElement.TryGetProperty("t", out JsonElement event_element))
{
throw new JsonException("Event name not found");
}
if (event_element.ValueKind != JsonValueKind.String)
{
throw new Exception("Event name not string");
}
string? event_name = event_element.GetString();
return event_name switch
{
"READY" => json_doc.Deserialize(SourceGenerationContext.Default.ReadyPacket),
"CHANNEL_CREATE" => json_doc.Deserialize(
SourceGenerationContext.Default.ChannelCreatePacket),
"CHANNEL_UPDATE" => json_doc.Deserialize(
SourceGenerationContext.Default.ChannelUpdatePacket),
"CHANNEL_DELETE" => json_doc.Deserialize(
SourceGenerationContext.Default.ChannelDeletePacket),
_ => throw new NotSupportedException($"Packet {event_name} is not supported in json deserialization")
};
default:
throw new NotSupportedException($"Opcode {op} is not supported in json deserialization");
}
} }
public override void Write(Utf8JsonWriter writer, GatewayPacket value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, GatewayPacket value, JsonSerializerOptions options)
@ -64,8 +66,14 @@ internal class GatewayPacketConverter : JsonConverter<GatewayPacket>
case IdentifyPacket id_packet: case IdentifyPacket id_packet:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(id_packet, SourceGenerationContext.Default.IdentifyPacket)); writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(id_packet, SourceGenerationContext.Default.IdentifyPacket));
break; break;
case HeartbeatPacket heartbeat_packet:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(heartbeat_packet, SourceGenerationContext.Default.HeartbeatPacket));
break;
case ResumePacket resume_packet:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.ResumePacket));
break;
default: default:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.IdentifyPacket)); writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.GatewayPacket));
break; break;
} }
} }

View File

@ -0,0 +1,6 @@
namespace Discord.API;
internal class HeartbeatAckPacket : GatewayPacket
{
public override Opcode Op => Opcode.HeartbeatAck;
}

View File

@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace Discord.API;
internal class HeartbeatPacket : GatewayPacket
{
public override Opcode Op => Opcode.Heartbeat;
[JsonRequired]
[JsonPropertyName("d")]
public required ulong? Sequence { get; init; }
}

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Discord.API;
internal class HelloPacket : GatewayPacket
{
public override Opcode Op => Opcode.Hello;
public struct HelloData
{
[JsonRequired]
public uint HeartbeatInterval { init; get; }
}
[JsonRequired]
[JsonPropertyName("d")]
public HelloData Data { init; get; }
}

View File

@ -5,8 +5,10 @@ namespace Discord.API;
internal class IdentifyPacket : GatewayPacket internal class IdentifyPacket : GatewayPacket
{ {
public sealed class IdentifyData public override Opcode Op => Opcode.Identify;
public struct IdentifyData
{ {
public IdentifyData() { }
public class PropertiesClass public class PropertiesClass
{ {
public string Os => Environment.OSVersion.ToString(); public string Os => Environment.OSVersion.ToString();
@ -27,10 +29,12 @@ internal class IdentifyPacket : GatewayPacket
public int LargeThreshold = 250; public int LargeThreshold = 250;
public int[]? Shard = null; public int[]? Shard = null;
//presence //presence
[JsonRequired]
public required ulong Intents { get; init; } public required ulong Intents { get; init; }
} }
[JsonPropertyName("d")] [JsonPropertyName("d")]
[JsonRequired]
public required IdentifyData Data { get; init; } public required IdentifyData Data { get; init; }
} }

View File

@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Discord.API;
internal class InvalidSessionPacket : GatewayPacket
{
public override Opcode Op => Opcode.InvalidSession;
[JsonPropertyName("d")]
public bool? Reconnect { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace Discord.API;
internal class ReconnectPacket : GatewayPacket
{
public override Opcode Op => Opcode.Reconnect;
}

View File

@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
namespace Discord.API;
internal class ResumePacket : GatewayPacket
{
public override Opcode Op => Opcode.Resume;
public struct ResumeData
{
[JsonRequired]
public required string Token { get; init; }
[JsonRequired]
public required string SessionId { get; init; }
[JsonRequired]
[JsonPropertyName("seq")]
public required ulong Sequence { get; init; }
}
[JsonRequired]
[JsonPropertyName("d")]
public required ResumeData Data { get; init; }
}

View File

@ -5,16 +5,11 @@ public interface IDiscordApi
public void OpenConnection(); public void OpenConnection();
public void Close(); public void Close();
public delegate void DiscordEventHandler<in T>(IDiscordApi sender, T data, LifeTime lifetime); public delegate void DiscordEventHandler<in T>(IDiscordApi sender, T data);
/*public event DiscordEventHandler<JObject>? OnReady; public event DiscordEventHandler<GatewayPacket>? OnPacketReceived;
public event DiscordEventHandler<JObject>? OnChannelCreate;
public event DiscordEventHandler<JObject>? OnChannelUpdate;
public event DiscordEventHandler<JObject>? OnChannelDelete;
public event DiscordEventHandler<JObject>? OnGuildCreate; public void SendPacket(GatewayPacket packet);
public event DiscordEventHandler<JObject>? OnGuildUpdate;
public event DiscordEventHandler<JObject>? OnGuildDelete;*/
} }