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 Newtonsoft.Json;
using Xunit.Abstractions;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Discord.API.Tests;
using Xunit;
@ -6,26 +9,132 @@ using Discord.API;
public class JsonTests
{
[Fact]
public void GatewayPacketEncodeJsonTest()
{
var gateway_packet = new GatewayPacket()
{
Op = GatewayPacket.Opcode.Heartbeat
};
private readonly ITestOutputHelper _testOutputHelper;
string json = JsonSerializer.Serialize(gateway_packet, SourceGenerationContext.Default.GatewayPacket);
Assert.Equal("""{"op":1}""", json);
public JsonTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Fact]
public void GatewayPacketDecodeTest()
public void InvalidSessionDeserialize()
{
const string src = """{"op":1}""";
var gateway_packet =
JsonSerializer.Deserialize<GatewayPacket>(src, SourceGenerationContext.Default.GatewayPacket);
Assert.Equal(GatewayPacket.Opcode.Heartbeat, gateway_packet?.Op);
string src = """
{
"op": 9
}
""";
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]
@ -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
{
[JsonRequired]
public ulong Id;
[JsonRequired]
public int Type;
public ulong? GuildId;
public int? Position;
public string? Name;
public string? Topic;
public bool? Nsfw;
public ulong? LastMessageId;
public int? Bitrate;
public ulong? ParentId;
public required ulong Id { get; init; }
public int? Type { get; init; }
public ulong? GuildId { get; init; }
public int? Position { get; init; }
public string? Name { get; init; }
public string? Topic { get; init; }
public bool? Nsfw { get; init; }
public ulong? LastMessageId { get; init; }
public int? Bitrate { get; init; }
public ulong? ParentId { get; init; }
//TODO: Missing fields
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,9 @@ namespace Discord.API;
internal class ChannelUpdatePacket : DispatchPacket
{
public override string Event => "CHANNEL_UPDATE";
[JsonRequired]
[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;
[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "t", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(ChannelCreatePacket))]
[JsonDerivedType(typeof(ChannelUpdatePacket))]
[JsonDerivedType(typeof(ChannelDeletePacket))]
[JsonDerivedType(typeof(ReadyPacket))]
internal class DispatchPacket : GatewayPacket
internal abstract class DispatchPacket : GatewayPacket
{
public override Opcode Op => Opcode.Dispatch;
[JsonPropertyName("t")]
[JsonRequired]
public string Event;
public abstract string Event { get; }
[JsonPropertyName("s")]
public ulong? Sequence;

View File

@ -1,5 +1,4 @@
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;
@ -7,28 +6,30 @@ namespace Discord.API;
[JsonDerivedType(typeof(ReadyPacket))]
internal class ReadyPacket : DispatchPacket
{
public sealed class ReadyData
public override string Event => "READY";
public struct ReadyData
{
[JsonRequired]
[JsonPropertyName("v")]
public int Version;
public required int Version { get; init; }
[JsonRequired]
public UserData User;
public required UserData User { get; init; }
[JsonRequired]
public UnavailableGuildData[] Guilds;
public required UnavailableGuildData[] Guilds { get; init; }
[JsonRequired]
public string SessionId;
public required string SessionId { get; init; }
[JsonRequired]
public string ResumeGatewayUrl;
public required string ResumeGatewayUrl { get; init; }
[JsonRequired]
public PartialApplicationData Application;
public required PartialApplicationData Application { get; init; }
}
[JsonRequired]
[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;
[JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true, TypeDiscriminatorPropertyName = "op", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(IdentifyPacket))]
[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
{
@ -23,7 +29,6 @@ internal class GatewayPacket
HeartbeatAck = 11
}
[JsonRequired]
[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.Serialization;
@ -8,53 +9,54 @@ internal class GatewayPacketConverter : JsonConverter<GatewayPacket>
public override GatewayPacket? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
JsonDocument json_doc = JsonDocument.ParseValue(ref reader);
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
if (!json_doc.RootElement.TryGetProperty("op", out JsonElement opcode_element))
{
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)
@ -64,8 +66,14 @@ internal class GatewayPacketConverter : JsonConverter<GatewayPacket>
case IdentifyPacket id_packet:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(id_packet, SourceGenerationContext.Default.IdentifyPacket));
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:
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.IdentifyPacket));
writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.GatewayPacket));
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
{
public sealed class IdentifyData
public override Opcode Op => Opcode.Identify;
public struct IdentifyData
{
public IdentifyData() { }
public class PropertiesClass
{
public string Os => Environment.OSVersion.ToString();
@ -27,10 +29,12 @@ internal class IdentifyPacket : GatewayPacket
public int LargeThreshold = 250;
public int[]? Shard = null;
//presence
[JsonRequired]
public required ulong Intents { get; init; }
}
[JsonPropertyName("d")]
[JsonRequired]
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 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<GatewayPacket>? OnPacketReceived;
public void SendPacket(GatewayPacket packet);
/*public event DiscordEventHandler<JObject>? OnReady;
public event DiscordEventHandler<JObject>? OnChannelCreate;
public event DiscordEventHandler<JObject>? OnChannelUpdate;
public event DiscordEventHandler<JObject>? OnChannelDelete;
public event DiscordEventHandler<JObject>? OnGuildCreate;
public event DiscordEventHandler<JObject>? OnGuildUpdate;
public event DiscordEventHandler<JObject>? OnGuildDelete;*/
}