diff --git a/Discord.API.Tests/JsonTests.cs b/Discord.API.Tests/JsonTests.cs index 9f44e89..76e016d 100644 --- a/Discord.API.Tests/JsonTests.cs +++ b/Discord.API.Tests/JsonTests.cs @@ -16,6 +16,156 @@ public class JsonTests _testOutputHelper = testOutputHelper; } + [Fact] + public void GuildCreateDeserialize() + { + string src = """ + { + "op": 0, + "t": "GUILD_CREATE", + "s": 3, + "d":{ + "id": "197038439483310086", + "name": "Discord Testers", + "icon": "f64c482b807da4f539cff778d174971c", + "description": "The official place to report Discord Bugs!", + "splash": null, + "discovery_splash": null, + "features": [ + "ANIMATED_ICON", + "VERIFIED", + "NEWS", + "VANITY_URL", + "DISCOVERABLE", + "MORE_EMOJI", + "INVITE_SPLASH", + "BANNER", + "COMMUNITY" + ], + "emojis": [], + "banner": "9b6439a7de04f1d26af92f84ac9e1e4a", + "owner_id": "73193882359173120", + "application_id": null, + "region": null, + "afk_channel_id": null, + "afk_timeout": 300, + "system_channel_id": null, + "widget_enabled": true, + "widget_channel_id": null, + "verification_level": 3, + "roles": [], + "default_message_notifications": 1, + "mfa_level": 1, + "explicit_content_filter": 2, + "max_presences": 40000, + "max_members": 250000, + "vanity_url_code": "discord-testers", + "premium_tier": 3, + "premium_subscription_count": 33, + "system_channel_flags": 0, + "preferred_locale": "en-US", + "rules_channel_id": "441688182833020939", + "public_updates_channel_id": "281283303326089216", + "safety_alerts_channel_id": "281283303326089216", + "joined_at": "2024-06-27T11:59:36Z", + "large": false, + "member_count": 69, + "voice_states": [ + { + "channel_id": "157733188964188161", + "user_id": "80351110224678912", + "session_id": "90326bd25d71d39b9ef95b299e3872ff", + "deaf": false, + "mute": false, + "self_deaf": false, + "self_mute": true, + "suppress": false, + "request_to_speak_timestamp": "2021-03-31T18:45:31.297561+00:00" + } + ], + "members":[ + { + "user": { + "id": "80351110224678912", + "username": "Nelly", + "discriminator": "1337", + "avatar": "8342729096ea3675442027381ff50dfe", + "verified": true, + "email": "nelly@discord.com", + "flags": 64, + "banner": "06c16474723fe537c283b8efa61a30c8", + "accent_color": 16711680, + "premium_type": 1, + "public_flags": 64, + "avatar_decoration_data": { + "sku_id": "1144058844004233369", + "asset": "a_fed43ab12698df65902ba06727e20c0e" + } + }, + "nick": "NOT API SUPPORT", + "avatar": null, + "roles": [], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "deaf": false, + "mute": false + } + ], + "channels":[ + { + "id": "41771983423143937", + "guild_id": "197038439483310086", + "name": "general", + "type": 0, + "position": 6, + "permission_overwrites": [], + "rate_limit_per_user": 2, + "nsfw": true, + "topic": "24/7 chat about how to gank Mike #2", + "last_message_id": "155117677105512449", + "parent_id": "399942396007890945", + "default_auto_archive_duration": 60 + } + ] + } + } + """; + + GatewayPacket? gateway_packet = JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket); + Assert.IsType(gateway_packet); + Assert.IsType((gateway_packet as GuildCreatePacket)!.Data); + + } + + [Fact] + public void UnavailableGuildCreateDeserialize() + { + string src = """ + { + "op": 0, + "t": "GUILD_CREATE", + "s": 3, + "d":{ + "id": "922243411795390566", + "unavailable": true + } + } + """; + + GatewayPacket? packet = + JsonSerializer.Deserialize(src, SourceGenerationContext.Default.GatewayPacket); + + Assert.IsType(packet); + Assert.IsType(((GuildCreatePacket)packet).Data); + Assert.True(packet is GuildCreatePacket + { + Data: UnavailableGuildData + { + Id: 922243411795390566, + Unavailable: true + } + }); + } + [Fact] public void InvalidSessionDeserialize() { diff --git a/Discord.API/DataTypes/GuildData.cs b/Discord.API/DataTypes/GuildData.cs new file mode 100644 index 0000000..829b162 --- /dev/null +++ b/Discord.API/DataTypes/GuildData.cs @@ -0,0 +1,20 @@ +namespace Discord.API; + +public class GuildData : UnavailableGuildData +{ + public override bool Unavailable => false; + public string? Name { get; init; } + public ulong? AfkChannelId { get; init; } + public int? AfkTimeout { get; init; } + public RoleData[]? Roles { get; init; } + public ulong? SystemChannelId { get; init; } + public uint? SystemChannelFlags { get; init; } + public string? Description { get; init; } + public int? NsfwLevel { get; init; } + public DateTime? JoinedAt { get; init; } + public bool? Large { get; init; } + public uint? MemberCount { get; init; } + public VoiceStateData[]? VoiceStates { get; init; } + public GuildMemberData[]? Members { get; init; } + public ChannelData[]? Channels { get; init; } +} \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildDataConverter.cs b/Discord.API/DataTypes/GuildDataConverter.cs new file mode 100644 index 0000000..9cdfed3 --- /dev/null +++ b/Discord.API/DataTypes/GuildDataConverter.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Discord.API; + +internal class GuildDataConverter : JsonConverter +{ + public override UnavailableGuildData? Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + JsonDocument json_doc = JsonDocument.ParseValue(ref reader); + if (!json_doc.RootElement.TryGetProperty("unavailable", out var unavailable_prop) || + unavailable_prop.ValueKind == JsonValueKind.False) + { + return json_doc.Deserialize(SourceGenerationContext.Default.GuildData); + } + else + { + if (json_doc.RootElement.TryGetProperty("id", out var id_prop) && + id_prop.ValueKind is JsonValueKind.String or JsonValueKind.Number) + { + ulong id = id_prop.ValueKind switch + { + JsonValueKind.Number => id_prop.GetUInt64(), + JsonValueKind.String => ulong.Parse(id_prop.GetString() ?? "") + }; + return new UnavailableGuildData() + { + Id = id + }; + } + else + { + throw new JsonException("Id property not found in guild data"); + } + } + } + + public override void Write(Utf8JsonWriter writer, UnavailableGuildData value, JsonSerializerOptions options) + { + if (value is GuildData) + { + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.GuildData)); + } + else + { + writer.WriteStartObject(); + writer.WriteBoolean("unavailable", true); + } + } +} \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildMemberData.cs b/Discord.API/DataTypes/GuildMemberData.cs new file mode 100644 index 0000000..9df242d --- /dev/null +++ b/Discord.API/DataTypes/GuildMemberData.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Discord.API; + +public class GuildMemberData +{ + public UserData? User { get; init; } + public string? Nick { get; init; } + public ulong[]? Roles { get; init; } + public DateTime? JoinedAt { get; init; } + public bool? Deaf { get; init; } + public bool? Mute { get; init; } + //TODO: More fields +} \ No newline at end of file diff --git a/Discord.API/DataTypes/RoleData.cs b/Discord.API/DataTypes/RoleData.cs new file mode 100644 index 0000000..2d23e5f --- /dev/null +++ b/Discord.API/DataTypes/RoleData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Discord.API; + +public class RoleData +{ + [JsonRequired] + public ulong Id { get; init; } + public string? Name { get; init; } + public uint? Color { get; init; } + public bool? Hoist { get; init; } + public int? Position { get; init; } + public bool? Managed { get; init; } + public bool? Mentionable { get; init; } +} \ No newline at end of file diff --git a/Discord.API/DataTypes/UnavailableGuildData.cs b/Discord.API/DataTypes/UnavailableGuildData.cs index debaad5..f7e6f20 100644 --- a/Discord.API/DataTypes/UnavailableGuildData.cs +++ b/Discord.API/DataTypes/UnavailableGuildData.cs @@ -2,8 +2,10 @@ using System.Text.Json.Serialization; namespace Discord.API; -public sealed class UnavailableGuildData +[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] +[JsonDerivedType(typeof(GuildData))] +public class UnavailableGuildData { public required ulong Id { get; init; } - public bool Unavailable { get; init; } + public virtual bool Unavailable => true; } \ No newline at end of file diff --git a/Discord.API/DataTypes/VoiceStateData.cs b/Discord.API/DataTypes/VoiceStateData.cs new file mode 100644 index 0000000..6fc7a0b --- /dev/null +++ b/Discord.API/DataTypes/VoiceStateData.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Discord.API; + +public class VoiceStateData +{ + public ulong? GuildId { get; init; } + public ulong? ChannelId { get; init; } + [JsonRequired] + public required ulong UserId { get; init; } + public GuildMemberData? Member { get; init; } + public string? SessionId { get; init; } + public bool? Mute { get; init; } + public bool? Deaf { get; init; } + public bool? SelfMute { get; init; } + public bool? SelfDeaf { get; init; } + public bool? SelfStream { get; init; } + public bool? SelfVideo { get; init; } + public bool? Suppress { get; init; } + public DateTime? RequestToSpeakTimestamp { get; init; } +} \ No newline at end of file diff --git a/Discord.API/Discord.API.csproj b/Discord.API/Discord.API.csproj index 4aabd55..8e08e06 100644 --- a/Discord.API/Discord.API.csproj +++ b/Discord.API/Discord.API.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - api + Discord.API diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs index aa833ea..59c65a7 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/DispatchPacket.cs @@ -8,6 +8,7 @@ namespace Discord.API; [JsonDerivedType(typeof(ChannelUpdatePacket))] [JsonDerivedType(typeof(ChannelDeletePacket))] [JsonDerivedType(typeof(ReadyPacket))] +[JsonDerivedType(typeof(GuildCreatePacket))] internal abstract class DispatchPacket : GatewayPacket { public override Opcode Op => Opcode.Dispatch; diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs new file mode 100644 index 0000000..edbfd51 --- /dev/null +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace Discord.API; + +internal class GuildCreatePacket : DispatchPacket +{ + public override string Event => "GUILD_CREATE"; + [JsonRequired] + [JsonPropertyName("d")] + public required UnavailableGuildData Data { get; init; } +} \ No newline at end of file diff --git a/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs index 906eced..26497d7 100644 --- a/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs +++ b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs @@ -52,6 +52,8 @@ internal class GatewayPacketConverter : JsonConverter SourceGenerationContext.Default.ChannelUpdatePacket), "CHANNEL_DELETE" => json_doc.Deserialize( SourceGenerationContext.Default.ChannelDeletePacket), + "GUILD_CREATE" => json_doc.Deserialize( + SourceGenerationContext.Default.GuildCreatePacket), _ => throw new NotSupportedException($"Packet {event_name} is not supported in json deserialization") }; default: diff --git a/Discord.API/SourceGenerationContext.cs b/Discord.API/SourceGenerationContext.cs index 80ffd15..950be11 100644 --- a/Discord.API/SourceGenerationContext.cs +++ b/Discord.API/SourceGenerationContext.cs @@ -10,7 +10,7 @@ using System.Text.Json; IgnoreReadOnlyProperties = false, IncludeFields = true, PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, - Converters = [typeof(GatewayPacketConverter)], + Converters = [typeof(GatewayPacketConverter), typeof(GuildDataConverter)], NumberHandling = JsonNumberHandling.AllowReadingFromString )] [JsonSerializable(typeof(GatewayPacket))]