diff --git a/Discord.API.Tests/JsonTests.cs b/Discord.API.Tests/JsonTests.cs index 35516ec..246ac32 100644 --- a/Discord.API.Tests/JsonTests.cs +++ b/Discord.API.Tests/JsonTests.cs @@ -150,6 +150,7 @@ public class JsonTests "user": { "id": "80351110224678912", "username": "Nelly", + "global_name": null, "discriminator": "1337", "avatar": "8342729096ea3675442027381ff50dfe", "verified": true, diff --git a/Discord.API/DataTypes/ChannelData.cs b/Discord.API/DataTypes/ChannelData.cs index a55c69b..0805a2d 100644 --- a/Discord.API/DataTypes/ChannelData.cs +++ b/Discord.API/DataTypes/ChannelData.cs @@ -6,13 +6,5 @@ public class ChannelData { public required ulong Id { get; init; } public required 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 } \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildChannelData.cs b/Discord.API/DataTypes/GuildChannelData.cs new file mode 100644 index 0000000..bc112ed --- /dev/null +++ b/Discord.API/DataTypes/GuildChannelData.cs @@ -0,0 +1,11 @@ +namespace Discord.API; + +public class GuildChannelData : ChannelData +{ + public ulong? GuildId { get; init; } + public int? Position { get; init; } + public required string Name { get; init; } + public ulong? ParentId { get; init; } + public int? Bitrate { get; init; } + public bool? Nsfw { get; init; } +} diff --git a/Discord.API/DataTypes/GuildCreateData.cs b/Discord.API/DataTypes/GuildCreateData.cs index 56a0cfd..489599d 100644 --- a/Discord.API/DataTypes/GuildCreateData.cs +++ b/Discord.API/DataTypes/GuildCreateData.cs @@ -5,6 +5,14 @@ public class GuildCreateData : GuildData{ public required bool Large { get; init; } public required uint MemberCount { get; init; } public required VoiceStateData[] VoiceStates { get; init; } - public required GuildMemberData[] Members { get; init; } + public required GuildMemberDataWithUser[] Members { get; init; } public required ChannelData[] Channels { get; init; } + + public override void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(VoiceStates); + ArgumentNullException.ThrowIfNull(Members); + ArgumentNullException.ThrowIfNull(Channels); + base.OnDeserialized(); + } } \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildData.cs b/Discord.API/DataTypes/GuildData.cs index fe9098f..abd94e3 100644 --- a/Discord.API/DataTypes/GuildData.cs +++ b/Discord.API/DataTypes/GuildData.cs @@ -17,4 +17,11 @@ public abstract class GuildData : UnavailableGuildData public required uint SystemChannelFlags { get; init; } public required string? Description { get; init; } public required int NsfwLevel { get; init; } + + public override void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(Name); + ArgumentNullException.ThrowIfNull(Roles); + base.OnDeserialized(); + } } \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildMemberData.cs b/Discord.API/DataTypes/GuildMemberData.cs index 9df242d..866a7f6 100644 --- a/Discord.API/DataTypes/GuildMemberData.cs +++ b/Discord.API/DataTypes/GuildMemberData.cs @@ -2,13 +2,21 @@ using System.Text.Json.Serialization; namespace Discord.API; -public class GuildMemberData +[JsonPolymorphic] +[JsonDerivedType(typeof(GuildMemberDataWithUser))] +public class GuildMemberData : IJsonOnDeserialized { - 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; } + public required ulong[] Roles { get; init; } + public required DateTime JoinedAt { get; init; } + public required bool Deaf { get; init; } + public required bool Mute { get; init; } + + public virtual void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(Roles); + } //TODO: More fields + + } \ No newline at end of file diff --git a/Discord.API/DataTypes/GuildMemberDataWithUser.cs b/Discord.API/DataTypes/GuildMemberDataWithUser.cs new file mode 100644 index 0000000..4dcc702 --- /dev/null +++ b/Discord.API/DataTypes/GuildMemberDataWithUser.cs @@ -0,0 +1,12 @@ +namespace Discord.API; + +public class GuildMemberDataWithUser : GuildMemberData +{ + public required UserData User { get; init; } + + public override void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(User); + base.OnDeserialized(); + } +} diff --git a/Discord.API/DataTypes/RoleData.cs b/Discord.API/DataTypes/RoleData.cs index 2d23e5f..0231339 100644 --- a/Discord.API/DataTypes/RoleData.cs +++ b/Discord.API/DataTypes/RoleData.cs @@ -2,14 +2,18 @@ using System.Text.Json.Serialization; namespace Discord.API; -public class RoleData +public class RoleData : IJsonOnDeserialized { - [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; } + public required ulong Id { get; init; } + public required string Name { get; init; } + public required uint Color { get; init; } + public required bool Hoist { get; init; } + public required int Position { get; init; } + public required bool Managed { get; init; } + public required bool Mentionable { get; init; } + + public void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(Name); + } } \ No newline at end of file diff --git a/Discord.API/DataTypes/UnavailableGuildData.cs b/Discord.API/DataTypes/UnavailableGuildData.cs index f7e6f20..d7f7c47 100644 --- a/Discord.API/DataTypes/UnavailableGuildData.cs +++ b/Discord.API/DataTypes/UnavailableGuildData.cs @@ -4,8 +4,12 @@ namespace Discord.API; [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] [JsonDerivedType(typeof(GuildData))] -public class UnavailableGuildData +public class UnavailableGuildData : IJsonOnDeserialized { public required ulong Id { get; init; } public virtual bool Unavailable => true; + + public virtual void OnDeserialized() + { + } } \ No newline at end of file diff --git a/Discord.API/DataTypes/UserData.cs b/Discord.API/DataTypes/UserData.cs index 506e679..fb72b33 100644 --- a/Discord.API/DataTypes/UserData.cs +++ b/Discord.API/DataTypes/UserData.cs @@ -1,13 +1,20 @@ 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; -public sealed class UserData +public sealed class UserData : IJsonOnDeserialized { public required ulong Id { get; init; } - public string? Username { get; init; } - public string? Discriminator { get; init; } + public required string Username { get; init; } + public required string Discriminator { get; init; } public string? GlobalName { get; init; } + + public void OnDeserialized() + { + ArgumentNullException.ThrowIfNull(Username); + ArgumentNullException.ThrowIfNull(Discriminator); + } //TODO More fields + + } \ No newline at end of file diff --git a/Discord.API/DataTypes/VoiceStateData.cs b/Discord.API/DataTypes/VoiceStateData.cs index 6fc7a0b..56913b9 100644 --- a/Discord.API/DataTypes/VoiceStateData.cs +++ b/Discord.API/DataTypes/VoiceStateData.cs @@ -4,18 +4,15 @@ namespace Discord.API; public class VoiceStateData { - public ulong? GuildId { get; init; } - public ulong? ChannelId { get; init; } - [JsonRequired] + public required ulong? ChannelId { get; init; } 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 required bool Mute { get; init; } + public required bool Deaf { get; init; } + public required bool SelfMute { get; init; } + public required bool SelfDeaf { get; init; } public bool? SelfStream { get; init; } public bool? SelfVideo { get; init; } - public bool? Suppress { get; init; } + public required bool Suppress { get; init; } public DateTime? RequestToSpeakTimestamp { get; init; } } \ No newline at end of file diff --git a/Discord.API/DataTypes/VoiceStateDataWithGuildId.cs b/Discord.API/DataTypes/VoiceStateDataWithGuildId.cs new file mode 100644 index 0000000..4abd889 --- /dev/null +++ b/Discord.API/DataTypes/VoiceStateDataWithGuildId.cs @@ -0,0 +1,6 @@ +namespace Discord.API; + +public class VoiceStateDataWithGuildId : VoiceStateData +{ + public required ulong GuildId {get; init;} +} diff --git a/Discord.API/Gateway/GatewayClient.cs b/Discord.API/Gateway/GatewayClient.cs index db5978e..2202244 100644 --- a/Discord.API/Gateway/GatewayClient.cs +++ b/Discord.API/Gateway/GatewayClient.cs @@ -59,6 +59,7 @@ public class GatewayClient : AbstractGateway { } protected override void MessageReceivedHandler(ResponseMessage msg){ if(msg.MessageType != System.Net.WebSockets.WebSocketMessageType.Text) return; + Log.Debug("GATEWAY PACKET: {packet}", msg); try{ GatewayPacket packet = JsonSerializer.Deserialize(msg.Text!, SourceGenerationContext.Default.GatewayPacket) ?? throw new Exception("Failed to deserialize packet"); // This can be optimized //TODO diff --git a/model/Discord.Model.csproj b/Discord.Model/Discord.Model.csproj similarity index 67% rename from model/Discord.Model.csproj rename to Discord.Model/Discord.Model.csproj index fa71b7a..7b2ba8a 100644 --- a/model/Discord.Model.csproj +++ b/Discord.Model/Discord.Model.csproj @@ -1,5 +1,9 @@  + + + + net8.0 enable diff --git a/Discord.Model/DiscordModel.cs b/Discord.Model/DiscordModel.cs new file mode 100644 index 0000000..06245a0 --- /dev/null +++ b/Discord.Model/DiscordModel.cs @@ -0,0 +1,22 @@ +using Discord.API; + +namespace Discord.Model; + +public sealed class DiscordModel +{ + public DiscordClient DiscordClient {get;} + private IDisposable Subscription; + public DiscordModel(string api_key, Intents intents){ + DiscordClient = new DiscordClient(api_key, intents); + Subscription = DiscordClient.PacketReceived.Subscribe(PacketHandler); + } + + private void PacketHandler(GatewayPacket packet){ + + } + + public void Close(){ + Subscription.Dispose(); + DiscordClient.Close().Wait(); + } +} diff --git a/Discord.Model/Types/Channel.cs b/Discord.Model/Types/Channel.cs new file mode 100644 index 0000000..73beeb7 --- /dev/null +++ b/Discord.Model/Types/Channel.cs @@ -0,0 +1,38 @@ +using Discord.API; + +namespace Discord.Model; + +public class Channel +{ + public enum ChannelType { + GuildText = 0, + DM = 1, + GuildVoice = 2, + GroupDM = 3, + GuildCategory = 4, + GuildAnnouncement = 5, + AnnouncementThread = 10, + PublicThread = 11, + PrivateThread = 12, + GuildStageVoice = 13, + GuildDirectory = 14, + GuildForum = 15, + GuildMedia = 16 + } + + public Snowflake Id { get; } + public ChannelType Type { get; protected set; } + + public Channel(ChannelData data){ + Id = data.Id; + Update(data, false); + } + + protected void Update(ChannelData data, bool call_base_update){ + Type = (ChannelType)data.Type; + } + + public virtual void Update(ChannelData data){ + Update(data, true); + } +} diff --git a/Discord.Model/Types/GuildChannel.cs b/Discord.Model/Types/GuildChannel.cs new file mode 100644 index 0000000..7072f3a --- /dev/null +++ b/Discord.Model/Types/GuildChannel.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; +using Discord.API; +using Serilog; + +namespace Discord.Model; + +public class GuildChannel : Channel +{ + public string Name {get; protected set;} + public Snowflake? ParentId {get; protected set;} + + internal GuildChannel(GuildChannelData data) : base(data){ + Update(data, false); + } + + [MemberNotNull(nameof(Name))] + protected void Update(GuildChannelData data, bool call_base_update) + { + if((ChannelType)data.Type is not ChannelType.GuildText or + ChannelType.GuildVoice or + ChannelType.GuildCategory or + ChannelType.GuildAnnouncement or + ChannelType.AnnouncementThread or + ChannelType.PublicThread or + ChannelType.PrivateThread or + ChannelType.GuildStageVoice or + ChannelType.GuildForum or + ChannelType.GuildMedia) + { + Log.Warning("GuildChannel has type {type} that is not compatible", (ChannelType)data.Type); + } + + Name = data.Name; + ParentId = data.ParentId; + + if(call_base_update) base.Update(data); + } + + public override void Update(ChannelData data) + { + if(data is GuildChannelData guild_data){ + Update(guild_data, true); + }else{ + throw new ArgumentException("data must be GuildChannelData", nameof(data)); + } + } +} diff --git a/Discord.Model/Types/GuildMember.cs b/Discord.Model/Types/GuildMember.cs new file mode 100644 index 0000000..e9a7ada --- /dev/null +++ b/Discord.Model/Types/GuildMember.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using Discord.API; + +namespace Discord.Model; + +public class GuildMember +{ + public Snowflake Id => User.Id; + public User User {get; } + public string? Nick { get; private set; } + public Snowflake[] Roles { get; private set; } + public DateTime JoinedAt { get; private set; } + public bool Deaf { get; private set; } + public bool Mute { get; private set; } + + internal GuildMember(GuildMemberData data, User user){ + User = user; + Update(data); + } + + [MemberNotNull(nameof(Roles))] + public void Update(GuildMemberData data){ + Nick = data.Nick; + Roles = data.Roles.Select(num => (Snowflake)num).ToArray(); + JoinedAt = data.JoinedAt; + Deaf = data.Deaf; + Mute = data.Mute; + } +} diff --git a/Discord.Model/Types/GuildTextChannel.cs b/Discord.Model/Types/GuildTextChannel.cs new file mode 100644 index 0000000..1adcb29 --- /dev/null +++ b/Discord.Model/Types/GuildTextChannel.cs @@ -0,0 +1,12 @@ +using Discord.API; + +namespace Discord.Model; + +public class GuildTextChannel : GuildChannel +{ + + internal GuildTextChannel(GuildChannelData data) : base(data){ + + } + +} diff --git a/Discord.Model/Types/Role.cs b/Discord.Model/Types/Role.cs new file mode 100644 index 0000000..38a50e2 --- /dev/null +++ b/Discord.Model/Types/Role.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using Discord.API; + +namespace Discord.Model; + +public sealed class Role +{ + public Snowflake Id { get; private set; } + public string Name { get; private set; } + public uint Color { get; private set; } + public bool Hoist { get; private set; } + public int Position { get; private set; } + public bool Managed { get; private set; } + public bool Mentionable { get; private set; } + + public Role(RoleData data){ + this.Id = data.Id; + Update(data); + } + + [MemberNotNull(nameof(Name))] + private void Update(RoleData data){ + this.Name = data.Name; + this.Color = data.Color; + this.Hoist = data.Hoist; + this.Position = data.Position; + this.Managed = data.Managed; + this.Mentionable = data.Mentionable; + } +} diff --git a/Discord.Model/Types/Snowflake.cs b/Discord.Model/Types/Snowflake.cs new file mode 100644 index 0000000..aa170db --- /dev/null +++ b/Discord.Model/Types/Snowflake.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Discord.Model; + +public readonly struct Snowflake +{ + public readonly ulong Number; + public Snowflake(ulong number){ + this.Number=number; + } + + public static bool operator ==(Snowflake left, Snowflake right){ + return left.Number == right.Number; + } + public static bool operator !=(Snowflake left, Snowflake right){ + return left.Number != right.Number; + } + + public static implicit operator Snowflake(ulong num){ + return new Snowflake(num); + } + + public override bool Equals([NotNullWhen(true)] object? obj) + { + if(obj is ulong num) return this.Number == num; + if(obj is Snowflake other) return this == other; + return false; + } + + public override int GetHashCode() + { + return (int)Number; + } + + public override string ToString() + { + return Number.ToString(); + } +} diff --git a/Discord.Model/Types/User.cs b/Discord.Model/Types/User.cs new file mode 100644 index 0000000..d95faea --- /dev/null +++ b/Discord.Model/Types/User.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using Discord.API; + +namespace Discord.Model; + +public class User +{ + public Snowflake Id {get;} + public string Username {get; private set;} + public string Discriminator {get; private set;} + public string? GlobalName {get; private set;} + + internal User(UserData data){ + this.Id = data.Id; + Update(data); + } + + [MemberNotNull(nameof(Username))] + [MemberNotNull(nameof(Discriminator))] + internal void Update(UserData data){ + this.Username = data.Username; + this.Discriminator = data.Discriminator; + this.GlobalName = data.GlobalName; + } +} diff --git a/Discord.Model/Types/VoiceState.cs b/Discord.Model/Types/VoiceState.cs new file mode 100644 index 0000000..2b5c4c8 --- /dev/null +++ b/Discord.Model/Types/VoiceState.cs @@ -0,0 +1,36 @@ +using Discord.API; + +namespace Discord.Model; + +public class VoiceState +{ + public ulong? ChannelId { get; private set; } + public ulong UserId { get; private set; } + public string? SessionId { get; private set; } + public bool Mute { get; private set; } + public bool Deaf { get; private set; } + public bool SelfMute { get; private set; } + public bool SelfDeaf { get; private set; } + public bool? SelfStream { get; private set; } + public bool? SelfVideo { get; private set; } + public bool Suppress { get; private set; } + public DateTime? RequestToSpeakTimestamp { get; private set; } + + public VoiceState(VoiceStateData data){ + UserId = data.UserId; + Update(data); + } + + public void Update(VoiceStateData data){ + ChannelId = data.ChannelId; + SessionId = data.SessionId; + Mute = data.Mute; + Deaf = data.Deaf; + SelfMute = data.SelfMute; + SelfDeaf = data.SelfDeaf; + SelfStream = data.SelfStream; + SelfVideo = data.SelfVideo; + Suppress = data.Suppress; + RequestToSpeakTimestamp = data.RequestToSpeakTimestamp; + } +} diff --git a/discord_api.sln b/discord_api.sln index 6894c7c..f5dd675 100644 --- a/discord_api.sln +++ b/discord_api.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Model", "model\Discord.Model.csproj", "{2FA8D012-CC43-4FBC-9520-CF627AB4BBEA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Model", "Discord.Model\Discord.Model.csproj", "{2FA8D012-CC43-4FBC-9520-CF627AB4BBEA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.API", "Discord.API\Discord.API.csproj", "{5C77661B-670F-400B-AF77-E3EC062B673D}" EndProject diff --git a/model/Class1.cs b/model/Class1.cs deleted file mode 100644 index 9bef5b7..0000000 --- a/model/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace model; - -public class Class1 -{ - -}