Compare commits

...

2 Commits

Author SHA1 Message Date
e925e541fd Started creating the model 2024-07-24 22:27:47 +02:00
79dda056b1 Instant heartbeat support 2024-07-24 11:44:09 +02:00
26 changed files with 391 additions and 45 deletions

View File

@ -150,6 +150,7 @@ public class JsonTests
"user": { "user": {
"id": "80351110224678912", "id": "80351110224678912",
"username": "Nelly", "username": "Nelly",
"global_name": null,
"discriminator": "1337", "discriminator": "1337",
"avatar": "8342729096ea3675442027381ff50dfe", "avatar": "8342729096ea3675442027381ff50dfe",
"verified": true, "verified": true,

View File

@ -6,13 +6,5 @@ public class ChannelData
{ {
public required ulong Id { get; init; } public required ulong Id { get; init; }
public required int Type { 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 //TODO: Missing fields
} }

View File

@ -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; }
}

View File

@ -5,6 +5,14 @@ public class GuildCreateData : GuildData{
public required bool Large { get; init; } public required bool Large { get; init; }
public required uint MemberCount { get; init; } public required uint MemberCount { get; init; }
public required VoiceStateData[] VoiceStates { 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 required ChannelData[] Channels { get; init; }
public override void OnDeserialized()
{
ArgumentNullException.ThrowIfNull(VoiceStates);
ArgumentNullException.ThrowIfNull(Members);
ArgumentNullException.ThrowIfNull(Channels);
base.OnDeserialized();
}
} }

View File

@ -17,4 +17,11 @@ public abstract class GuildData : UnavailableGuildData
public required uint SystemChannelFlags { get; init; } public required uint SystemChannelFlags { get; init; }
public required string? Description { get; init; } public required string? Description { get; init; }
public required int NsfwLevel { get; init; } public required int NsfwLevel { get; init; }
public override void OnDeserialized()
{
ArgumentNullException.ThrowIfNull(Name);
ArgumentNullException.ThrowIfNull(Roles);
base.OnDeserialized();
}
} }

View File

@ -2,13 +2,21 @@ using System.Text.Json.Serialization;
namespace Discord.API; 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 string? Nick { get; init; }
public ulong[]? Roles { get; init; } public required ulong[] Roles { get; init; }
public DateTime? JoinedAt { get; init; } public required DateTime JoinedAt { get; init; }
public bool? Deaf { get; init; } public required bool Deaf { get; init; }
public bool? Mute { get; init; } public required bool Mute { get; init; }
public virtual void OnDeserialized()
{
ArgumentNullException.ThrowIfNull(Roles);
}
//TODO: More fields //TODO: More fields
} }

View File

@ -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();
}
}

View File

@ -2,14 +2,18 @@ using System.Text.Json.Serialization;
namespace Discord.API; namespace Discord.API;
public class RoleData public class RoleData : IJsonOnDeserialized
{ {
[JsonRequired] public required ulong Id { get; init; }
public ulong Id { get; init; } public required string Name { get; init; }
public string? Name { get; init; } public required uint Color { get; init; }
public uint? Color { get; init; } public required bool Hoist { get; init; }
public bool? Hoist { get; init; } public required int Position { get; init; }
public int? Position { get; init; } public required bool Managed { get; init; }
public bool? Managed { get; init; } public required bool Mentionable { get; init; }
public bool? Mentionable { get; init; }
public void OnDeserialized()
{
ArgumentNullException.ThrowIfNull(Name);
}
} }

View File

@ -4,8 +4,12 @@ namespace Discord.API;
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(GuildData))] [JsonDerivedType(typeof(GuildData))]
public class UnavailableGuildData public class UnavailableGuildData : IJsonOnDeserialized
{ {
public required ulong Id { get; init; } public required ulong Id { get; init; }
public virtual bool Unavailable => true; public virtual bool Unavailable => true;
public virtual void OnDeserialized()
{
}
} }

View File

@ -1,13 +1,20 @@
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;
public sealed class UserData public sealed class UserData : IJsonOnDeserialized
{ {
public required ulong Id { get; init; } public required ulong Id { get; init; }
public string? Username { get; init; } public required string Username { get; init; }
public string? Discriminator { get; init; } public required string Discriminator { get; init; }
public string? GlobalName { get; init; } public string? GlobalName { get; init; }
public void OnDeserialized()
{
ArgumentNullException.ThrowIfNull(Username);
ArgumentNullException.ThrowIfNull(Discriminator);
}
//TODO More fields //TODO More fields
} }

View File

@ -4,18 +4,15 @@ namespace Discord.API;
public class VoiceStateData public class VoiceStateData
{ {
public ulong? GuildId { get; init; } public required ulong? ChannelId { get; init; }
public ulong? ChannelId { get; init; }
[JsonRequired]
public required ulong UserId { get; init; } public required ulong UserId { get; init; }
public GuildMemberData? Member { get; init; }
public string? SessionId { get; init; } public string? SessionId { get; init; }
public bool? Mute { get; init; } public required bool Mute { get; init; }
public bool? Deaf { get; init; } public required bool Deaf { get; init; }
public bool? SelfMute { get; init; } public required bool SelfMute { get; init; }
public bool? SelfDeaf { get; init; } public required bool SelfDeaf { get; init; }
public bool? SelfStream { get; init; } public bool? SelfStream { get; init; }
public bool? SelfVideo { get; init; } public bool? SelfVideo { get; init; }
public bool? Suppress { get; init; } public required bool Suppress { get; init; }
public DateTime? RequestToSpeakTimestamp { get; init; } public DateTime? RequestToSpeakTimestamp { get; init; }
} }

View File

@ -0,0 +1,6 @@
namespace Discord.API;
public class VoiceStateDataWithGuildId : VoiceStateData
{
public required ulong GuildId {get; init;}
}

View File

@ -96,6 +96,10 @@ public abstract class AbstractGateway {
InstantHeartbeatCts?.Cancel(); InstantHeartbeatCts?.Cancel();
} }
protected void ImmediateHeartbeat(){
InstantHeartbeatCts?.Cancel();
}
#endregion #endregion
public virtual async Task Close(){ public virtual async Task Close(){

View File

@ -59,6 +59,7 @@ public class GatewayClient : AbstractGateway {
} }
protected override void MessageReceivedHandler(ResponseMessage msg){ protected override void MessageReceivedHandler(ResponseMessage msg){
if(msg.MessageType != System.Net.WebSockets.WebSocketMessageType.Text) return; if(msg.MessageType != System.Net.WebSockets.WebSocketMessageType.Text) return;
Log.Debug("GATEWAY PACKET: {packet}", msg);
try{ try{
GatewayPacket packet = JsonSerializer.Deserialize(msg.Text!, SourceGenerationContext.Default.GatewayPacket) GatewayPacket packet = JsonSerializer.Deserialize(msg.Text!, SourceGenerationContext.Default.GatewayPacket)
?? throw new Exception("Failed to deserialize packet"); // This can be optimized //TODO ?? throw new Exception("Failed to deserialize packet"); // This can be optimized //TODO
@ -78,6 +79,9 @@ public class GatewayClient : AbstractGateway {
case InvalidSessionPacket invalidSessionPacket: case InvalidSessionPacket invalidSessionPacket:
InvalidSessionHandler(invalidSessionPacket); InvalidSessionHandler(invalidSessionPacket);
break; break;
case HeartbeatPacket:
HeartbeatPacketHandler();
break;
default: default:
Log.Debug("GATEWAY: Packet not handled {opcode}", packet.Op); Log.Debug("GATEWAY: Packet not handled {opcode}", packet.Op);
break; break;
@ -126,6 +130,11 @@ public class GatewayClient : AbstractGateway {
Log.Debug("GATEWAY: Resume url: {url}", packet.Data.ResumeGatewayUrl); Log.Debug("GATEWAY: Resume url: {url}", packet.Data.ResumeGatewayUrl);
} }
private void HeartbeatPacketHandler(){
ImmediateHeartbeat();
Log.Debug("GATEWAY: Remote requested immediate heartbeat");
}
protected override Task SendHeartbeat() protected override Task SendHeartbeat()
{ {
HeartbeatPacket packet = new(){ HeartbeatPacket packet = new(){

View File

@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Discord.API\Discord.API.csproj" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,12 @@
using Discord.API;
namespace Discord.Model;
public class GuildTextChannel : GuildChannel
{
internal GuildTextChannel(GuildChannelData data) : base(data){
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.API", "Discord.API\Discord.API.csproj", "{5C77661B-670F-400B-AF77-E3EC062B673D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.API", "Discord.API\Discord.API.csproj", "{5C77661B-670F-400B-AF77-E3EC062B673D}"
EndProject EndProject

View File

@ -1,6 +0,0 @@
namespace model;
public class Class1
{
}