From 615a6b98fa5ef38ca033071eca852d8d223aca4b Mon Sep 17 00:00:00 2001 From: Laci0503 Date: Tue, 23 Jul 2024 14:34:33 +0200 Subject: [PATCH] Authentication flow implemented in Gateway --- Discord.API.Tests/JsonTests.cs | 1 - Discord.API/DataTypes/Intents.cs | 26 ++++++ Discord.API/DiscordClient.cs | 11 ++- Discord.API/Gateway/AbstractGateway.cs | 5 ++ Discord.API/Gateway/GatewayClient.cs | 85 +++++++++++++++++-- .../DispatchPacket/ChannelCreatePacket.cs | 1 + .../DispatchPacket/ChannelDeletePacket.cs | 1 + .../DispatchPacket/ChannelUpdatePacket.cs | 1 + .../DispatchPacket/GenericDispatchPacket.cs | 14 +++ .../DispatchPacket/GuildCreatePacket.cs | 1 + .../DispatchPacket/GuildUpdatePacket.cs | 1 + .../DispatchPacket/ReadyPacket.cs | 3 +- .../GatewayPacketConverter.cs | 7 +- .../GatewayPacketTypes/IdentifyPacket.cs | 2 +- .../InvalidSessionPacket.cs | 2 +- Discord.API/SourceGenerationContext.cs | 1 + example_bot/Program.cs | 6 +- example_bot/example_bot.csproj | 1 + 18 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 Discord.API/DataTypes/Intents.cs create mode 100644 Discord.API/GatewayPacketTypes/DispatchPacket/GenericDispatchPacket.cs diff --git a/Discord.API.Tests/JsonTests.cs b/Discord.API.Tests/JsonTests.cs index 432dca7..35516ec 100644 --- a/Discord.API.Tests/JsonTests.cs +++ b/Discord.API.Tests/JsonTests.cs @@ -336,7 +336,6 @@ public class JsonTests { Token = "token_lol", Intents = 6969, - Shard = [1, 2], LargeThreshold = 42 } }; diff --git a/Discord.API/DataTypes/Intents.cs b/Discord.API/DataTypes/Intents.cs new file mode 100644 index 0000000..3c83604 --- /dev/null +++ b/Discord.API/DataTypes/Intents.cs @@ -0,0 +1,26 @@ +namespace Discord.API; + +[Flags] +public enum Intents : ulong { + Guilds = 1, + GuildMembers = 2, + GuildModeration = 4, + GuildEmojisAndStickers = 8, + GuildIntegrations = 16, + GuildWebhooks = 32, + GuildInvites = 64, + GuildVoiceStates = 128, + GuildPresences = 256, + GuildMessages = 512, + GuildMessageReactions = 1024, + GuildMessageTyping = 2048, + DirectMessages = 4096, + DirectMessageReactions = 8192, + DirectMessageTyping = 16384, + MessageContent = 32768, + GuildScheduledEvents = 65536, + AutoModerationConfiguration = 1048576, + AutoModerationExecution = 2097152, + GuildMessagePolls = 16777216, + DirectMessagePolls = 33554432 +} \ No newline at end of file diff --git a/Discord.API/DiscordClient.cs b/Discord.API/DiscordClient.cs index 6370038..0892db4 100644 --- a/Discord.API/DiscordClient.cs +++ b/Discord.API/DiscordClient.cs @@ -8,12 +8,14 @@ public class DiscordClient private static readonly string ApiBaseUrl = "https://discord.com/api/v10"; //TODO Export version number to a separate constant private string ApiKey; + private readonly Intents Intents; private RestClient Rest; private GatewayClient? Gateway; private TimeProvider TimeProvider; - public DiscordClient(string api_key){ + public DiscordClient(string api_key, Intents intents){ TimeProvider = TimeProvider.System; + this.Intents=intents; this.ApiKey = api_key; Rest = new RestClient(ApiBaseUrl, ApiKey); Task.Run(ConnectGateway); @@ -43,8 +45,13 @@ public class DiscordClient Log.Information("DiscordClient: Gateway url: {gateway_url}", resp.Value.Url); - Gateway = new GatewayClient(resp.Value.Url, ApiKey, TimeProvider); + Gateway = new GatewayClient(resp.Value.Url, ApiKey, TimeProvider, (ulong)Intents); Log.Information("DiscordClient: Started gateway"); } + + public async Task Close(){ + if(Gateway is not null) + await Gateway.Close(); + } } diff --git a/Discord.API/Gateway/AbstractGateway.cs b/Discord.API/Gateway/AbstractGateway.cs index ec94772..0241548 100644 --- a/Discord.API/Gateway/AbstractGateway.cs +++ b/Discord.API/Gateway/AbstractGateway.cs @@ -96,4 +96,9 @@ public abstract class AbstractGateway { } #endregion + + public async Task Close(){ + StopHeartbeat(); + await WebsocketClient.Stop(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Shutdown"); + } } \ No newline at end of file diff --git a/Discord.API/Gateway/GatewayClient.cs b/Discord.API/Gateway/GatewayClient.cs index fdf76c6..933a379 100644 --- a/Discord.API/Gateway/GatewayClient.cs +++ b/Discord.API/Gateway/GatewayClient.cs @@ -9,16 +9,21 @@ public class GatewayClient : AbstractGateway { private const int ApiVersion = 10; - private string ApiKey; + private readonly string ApiKey; + private readonly ulong Intents; private ulong? Sequence = null; + private string? SessionId = null; + private Uri StartingUri; - public GatewayClient(string url, string api_key, TimeProvider time_provider) - : this(url, api_key, time_provider, uri => new WebsocketClient(uri)) + public GatewayClient(string url, string api_key, TimeProvider time_provider, ulong intents) + : this(url, api_key, time_provider, uri => new WebsocketClient(uri), intents) { } - internal GatewayClient(string url, string api_key, TimeProvider time_provider, Func websocket_client_factory) + internal GatewayClient(string url, string api_key, TimeProvider time_provider, Func websocket_client_factory, ulong intents) : base(websocket_client_factory.Invoke(BuildUrl(url)), time_provider){ this.ApiKey=api_key; + this.Intents = intents; + this.StartingUri = WebsocketClient.Url; Log.Debug("GATEWAY: Created new gateway, with url: {url}", WebsocketClient.Url); } @@ -33,6 +38,16 @@ public class GatewayClient : AbstractGateway { protected override void DisconnectHandler(DisconnectionInfo info){ Log.Information("GATEWAY: Disconnected. Type: {DisconnectionType}", info.Type); + Log.Debug("GATEWAY: Connection closed due to {code} {description}", info.CloseStatus, info.CloseStatusDescription); + + if((int?)info.CloseStatus is 4004 or 4008 or (>= 4010 and <= 4014)){ + Log.Error("GATEWAY: Disconnection due to error: close code: {code}, desc: {desc}", info.CloseStatus, info.CloseStatusDescription); + info.CancelReconnection = true; + }else if((int?)info.CloseStatus is 4007 or 4009){ + SessionId = null; + Sequence = null; + WebsocketClient.Url = StartingUri; + } } protected override void ReconnectHandler(ReconnectionInfo info){ Log.Information("GATEWAY: (Re)Connected to server. Url: {url}, Type: {Type}", WebsocketClient.Url, info.Type); @@ -43,7 +58,7 @@ public class GatewayClient : AbstractGateway { GatewayPacket packet = JsonSerializer.Deserialize(msg.Text!, SourceGenerationContext.Default.GatewayPacket) ?? throw new Exception("Failed to deserialize packet"); // This can be optimized //TODO - Log.Debug("GATEWAY: Packet received"); + Log.Debug("GATEWAY: Packet received, opcode: {opcode}", packet.Op); switch(packet){ case HelloPacket helloPacket: @@ -52,8 +67,14 @@ public class GatewayClient : AbstractGateway { case HeartbeatAckPacket: HeartbeatAckHandler(); break; + case DispatchPacket dispatchPacket: + DispatchHandler(dispatchPacket); + break; + case InvalidSessionPacket invalidSessionPacket: + InvalidSessionHandler(invalidSessionPacket); + break; default: - Log.Debug("GATEWAY: Packet not handled"); + Log.Debug("GATEWAY: Packet not handled {opcode}", packet.Op); break; } }catch(Exception ex){ @@ -64,6 +85,7 @@ public class GatewayClient : AbstractGateway { private void HelloPacketHandler(HelloPacket packet){ StartHeartbeat((int)packet.Data.HeartbeatInterval); Log.Debug("GATEWAY: Hello packet received, heartbeat interval: {heartbeat_ms}", packet.Data.HeartbeatInterval); + Login(); } private void HeartbeatAckHandler(){ @@ -71,6 +93,32 @@ public class GatewayClient : AbstractGateway { Log.Information("GATEWAY: Heartbeat ping: {ping_ms} ms", HeartbeatAckReceived()); } + private void DispatchHandler(DispatchPacket packet){ + Log.Debug("GATEWAY: Event: {event}, seq: {sequence}", packet.Event, packet.Sequence); + Sequence = packet.Sequence; + switch(packet){ + case ReadyPacket ready_packet: + ReadyHandler(ready_packet); + break; + } + } + + private void InvalidSessionHandler(InvalidSessionPacket packet){ + Log.Debug("GATEWAY: Invalid session resumable: {resumable}", packet.Resume); + if(packet.Resume != true){ // This look awful tbh + SessionId=null; + Sequence=null; + WebsocketClient.Url=StartingUri; + _ = WebsocketClient.Reconnect(); + } + } + + private void ReadyHandler(ReadyPacket packet){ + SessionId = packet.Data.SessionId; + WebsocketClient.Url = BuildUrl(packet.Data.ResumeGatewayUrl); + Log.Debug("GATEWAY: Resume url: {url}", packet.Data.ResumeGatewayUrl); + } + protected override Task SendHeartbeat() { HeartbeatPacket packet = new(){ @@ -87,4 +135,29 @@ public class GatewayClient : AbstractGateway { Log.Debug("GATEWAY: Heartbeat ack missed. Reconnecting"); return Task.FromResult(true); } + + private void Login(){ + if(SessionId != null && Sequence != null){ + // Resume + ResumePacket packet = new(){ + Data=new(){ + Token=ApiKey, + Sequence=Sequence.Value, + SessionId=SessionId + } + }; + WebsocketClient.Send(JsonSerializer.Serialize(packet, SourceGenerationContext.Default.ResumePacket)); + Log.Debug("GATEWAY: Resuming previus session"); + }else{ + // Identify + IdentifyPacket packet = new(){ + Data=new(){ + Intents = Intents, + Token = ApiKey + } + }; + WebsocketClient.Send(JsonSerializer.Serialize(packet, SourceGenerationContext.Default.IdentifyPacket)); + Log.Debug("GATEWAY: Identifying for a new session"); + } + } } diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs index 837ba6e..535b1ec 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelCreatePacket.cs @@ -5,6 +5,7 @@ namespace Discord.API; internal class ChannelCreatePacket : DispatchPacket { + [JsonIgnore] public override string Event => "CHANNEL_CREATE"; [JsonRequired] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs index 1fc4378..75733ce 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelDeletePacket.cs @@ -5,6 +5,7 @@ namespace Discord.API; internal class ChannelDeletePacket : DispatchPacket { + [JsonIgnore] public override string Event => "CHANNEL_DELETE"; [JsonRequired] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs index 2132e13..d77bf6c 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ChannelUpdatePacket.cs @@ -5,6 +5,7 @@ namespace Discord.API; internal class ChannelUpdatePacket : DispatchPacket { + [JsonIgnore] public override string Event => "CHANNEL_UPDATE"; [JsonRequired] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/GenericDispatchPacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/GenericDispatchPacket.cs new file mode 100644 index 0000000..17bb9c0 --- /dev/null +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/GenericDispatchPacket.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Discord.API; + +internal class GenericDispatchPacket : DispatchPacket +{ + private string _event; + [JsonIgnore] + public override string Event => _event; + public GenericDispatchPacket(string t, ulong? s){ + _event=t; + Sequence = s; + } +} \ No newline at end of file diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs index edbfd51..ba85505 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildCreatePacket.cs @@ -4,6 +4,7 @@ namespace Discord.API; internal class GuildCreatePacket : DispatchPacket { + [JsonIgnore] public override string Event => "GUILD_CREATE"; [JsonRequired] [JsonPropertyName("d")] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/GuildUpdatePacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildUpdatePacket.cs index a9f6365..2ec2762 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/GuildUpdatePacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/GuildUpdatePacket.cs @@ -4,6 +4,7 @@ namespace Discord.API; internal class GuildUpdatePacket : DispatchPacket { + [JsonIgnore] public override string Event => "GUILD_UPDATE"; [JsonRequired] [JsonPropertyName("d")] diff --git a/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs b/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs index 6077fb0..537b347 100644 --- a/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs +++ b/Discord.API/GatewayPacketTypes/DispatchPacket/ReadyPacket.cs @@ -2,10 +2,9 @@ using System.Text.Json.Serialization; namespace Discord.API; -[JsonPolymorphic(TypeDiscriminatorPropertyName = "t", IgnoreUnrecognizedTypeDiscriminators = true, UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ReadyPacket))] internal class ReadyPacket : DispatchPacket { + [JsonIgnore] public override string Event => "READY"; public struct ReadyData { diff --git a/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs index 42a4356..016aa15 100644 --- a/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs +++ b/Discord.API/GatewayPacketTypes/GatewayPacketConverter.cs @@ -1,4 +1,5 @@ using System.Reflection.Emit; +using System.Reflection.Metadata.Ecma335; using System.Text.Json; using System.Text.Json.Serialization; @@ -57,7 +58,8 @@ internal class GatewayPacketConverter : JsonConverter "GUILD_UPDATE" => json_doc.Deserialize( SourceGenerationContext.Default.GuildUpdatePacket ), - _ => throw new NotSupportedException($"Packet {event_name} is not supported in json deserialization") + not null => new GenericDispatchPacket(event_name, json_doc.RootElement.GetProperty("s").GetUInt64()), + _ => throw new JsonException("Event name was null") }; default: throw new NotSupportedException($"Opcode {op} is not supported in json deserialization"); @@ -78,7 +80,8 @@ internal class GatewayPacketConverter : JsonConverter writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.ResumePacket)); break; default: - writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.GatewayPacket)); + //writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value, SourceGenerationContext.Default.GatewayPacket)); + //this is bad //TODO break; } } diff --git a/Discord.API/GatewayPacketTypes/IdentifyPacket.cs b/Discord.API/GatewayPacketTypes/IdentifyPacket.cs index bd40e7f..db3030c 100644 --- a/Discord.API/GatewayPacketTypes/IdentifyPacket.cs +++ b/Discord.API/GatewayPacketTypes/IdentifyPacket.cs @@ -26,7 +26,7 @@ internal class IdentifyPacket : GatewayPacket public PropertiesClass Properties => PropertiesClass.Instance; [JsonInclude] public bool Compress => false; - public int LargeThreshold = 250; + public int? LargeThreshold = 250; public int[]? Shard = null; //presence [JsonRequired] diff --git a/Discord.API/GatewayPacketTypes/InvalidSessionPacket.cs b/Discord.API/GatewayPacketTypes/InvalidSessionPacket.cs index c609a35..670e796 100644 --- a/Discord.API/GatewayPacketTypes/InvalidSessionPacket.cs +++ b/Discord.API/GatewayPacketTypes/InvalidSessionPacket.cs @@ -6,5 +6,5 @@ internal class InvalidSessionPacket : GatewayPacket { public override Opcode Op => Opcode.InvalidSession; [JsonPropertyName("d")] - public bool? Reconnect { get; init; } + public bool? Resume { get; init; } } \ No newline at end of file diff --git a/Discord.API/SourceGenerationContext.cs b/Discord.API/SourceGenerationContext.cs index 38ddc1a..419482f 100644 --- a/Discord.API/SourceGenerationContext.cs +++ b/Discord.API/SourceGenerationContext.cs @@ -5,6 +5,7 @@ namespace Discord.API; [JsonSourceGenerationOptions(IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = false, IncludeFields = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, Converters = [ typeof(GatewayPacketConverter), diff --git a/example_bot/Program.cs b/example_bot/Program.cs index 2170101..e768e67 100644 --- a/example_bot/Program.cs +++ b/example_bot/Program.cs @@ -8,6 +8,8 @@ Log.Logger=new LoggerConfiguration() string api_key = File.ReadAllText("api_key.txt"); -DiscordClient client = new DiscordClient(api_key); +DiscordClient client = new DiscordClient(api_key, Intents.Guilds); -Console.ReadLine(); \ No newline at end of file +Console.ReadLine(); + +await client.Close(); \ No newline at end of file diff --git a/example_bot/example_bot.csproj b/example_bot/example_bot.csproj index 7655526..f7f37ec 100644 --- a/example_bot/example_bot.csproj +++ b/example_bot/example_bot.csproj @@ -14,6 +14,7 @@ net8.0 enable enable + true