diff --git a/src/StevesBot.Worker.Tests/.editorconfig b/src/StevesBot.Worker.Tests/.editorconfig index 4d6c5e6..7f3b3bd 100644 --- a/src/StevesBot.Worker.Tests/.editorconfig +++ b/src/StevesBot.Worker.Tests/.editorconfig @@ -6,3 +6,4 @@ dotnet_diagnostic.CA2007.severity = none dotnet_diagnostic.CA1303.severity = none dotnet_diagnostic.CA1031.severity = none dotnet_diagnostic.CA1034.severity = none +dotnet_diagnostic.CA1054.severity = none diff --git a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs b/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs index c461f6e..a3ba10d 100644 --- a/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/DiscordGatewayClientTests.cs @@ -145,7 +145,7 @@ public sealed class DiscordGatewayClientTests : IDisposable mockWebSocket .Verify( static x => x.ConnectAsync(It.IsAny(), It.IsAny()), - Times.Once + Times.AtLeastOnce ); } @@ -502,33 +502,572 @@ public sealed class DiscordGatewayClientTests : IDisposable } [Fact] - public Task ConnectAsync_OnceConnectedWhenUnReconnectableCloseStatusIsReceived_ItShouldDisconnectAndNotTryToReconnect() + public async Task ConnectAsync_OnceConnectedWhenUnReconnectableCloseStatusIsReceived_ItShouldDisconnectAndNotTryToReconnect() { - return Task.CompletedTask; + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var socketState = WebSocketState.Closed; + + var mockWebSocket = new Mock(); + + mockWebSocket + .Setup(static x => x.State) + .Returns(() => socketState); + + mockWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => socketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + mockWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => socketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var closeEvent = new DiscordEvent(); + var closePayload = CreateEventPayload(closeEvent); + var closeResult = new WebSocketReceiveResult( + closePayload.Bytes.Length, + WebSocketMessageType.Close, + true, + (WebSocketCloseStatus)4004, + "Closing" + ); + messagesToReceive.Enqueue((closeResult, closePayload.Bytes)); + + SetupReceiveMessageSequence(mockWebSocket, messagesToReceive); + + _mockWebSocketFactory + .Setup(static x => x.Create()) + .Returns(mockWebSocket.Object); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + mockWebSocket + .Verify( + x => x.CloseAsync( + It.Is(s => s == WebSocketCloseStatus.NormalClosure), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + + [Theory] + [InlineData("", "session_id", 1010, 0)] + [InlineData("wss://resume.discord.gg", "", 1010, 0)] + [InlineData("wss://resume.discord.gg", "session_id", 1010, 1)] + [InlineData("wss://resume.discord.gg", "session_id", 1001, 0)] + public async Task ConnectAsync_OnceConnectedWhenCloseStatusIsReceived_ItShouldResumeOrReconnectCorrectly( + string resumeGatewayUrl, + string sessionId, + int closureStatus, + int resumeCount + ) + { + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var initialWebSocketState = WebSocketState.Closed; + + var initialWebSocket = new Mock(); + + initialWebSocket + .Setup(static x => x.State) + .Returns(() => initialWebSocketState); + + initialWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + initialWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var resumeWebSocketState = WebSocketState.Closed; + + var resumingWebSocket = new Mock(); + + resumingWebSocket + .Setup(static x => x.State) + .Returns(() => resumeWebSocketState); + + resumingWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + resumingWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var closeEvent = new DiscordEvent(); + var closePayload = CreateEventPayload(closeEvent); + var closeResult = new WebSocketReceiveResult( + closePayload.Bytes.Length, + WebSocketMessageType.Close, + true, + (WebSocketCloseStatus)closureStatus, + "Closing" + ); + messagesToReceive.Enqueue((closeResult, closePayload.Bytes)); + + SetupReceiveMessageSequence(initialWebSocket, messagesToReceive); + + _mockWebSocketFactory + .SetupSequence(static x => x.Create()) + .Returns(initialWebSocket.Object) + .Returns(resumingWebSocket.Object); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + var expectedUri = new Uri("wss://resume.discord.gg/?v=10&encoding=json"); + + resumingWebSocket + .Verify( + x => x.ConnectAsync( + It.Is(uri => uri.Equals(expectedUri)), + It.IsAny() + ), + Times.Exactly(resumeCount) + ); } [Fact] - public Task ConnectAsync_OnceConnectedWhenReconnectableButNonResumableCloseStatusIsReceived_ItShouldReconnect() + public async Task ConnectAsync_OnceConnectedWhenReconnectEventIsReceived_ItShouldResume() { - return Task.CompletedTask; + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var initialWebSocketState = WebSocketState.Closed; + + var initialWebSocket = new Mock(); + + initialWebSocket + .Setup(static x => x.State) + .Returns(() => initialWebSocketState); + + initialWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + initialWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var resumeWebSocketState = WebSocketState.Closed; + + var resumingWebSocket = new Mock(); + + resumingWebSocket + .Setup(static x => x.State) + .Returns(() => resumeWebSocketState); + + resumingWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + resumingWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var reconnectEvent = new ReconnectDiscordEvent(); + var reconnectPayload = CreateEventPayload(reconnectEvent); + var reconnectResult = new WebSocketReceiveResult( + reconnectPayload.Bytes.Length, + WebSocketMessageType.Text, + true + ); + messagesToReceive.Enqueue((reconnectResult, reconnectPayload.Bytes)); + + SetupReceiveMessageSequence(initialWebSocket, messagesToReceive); + + _mockWebSocketFactory + .SetupSequence(static x => x.Create()) + .Returns(initialWebSocket.Object) + .Returns(resumingWebSocket.Object); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + var expectedUri = new Uri($"{resumeGatewayUrl}/?v=10&encoding=json"); + + resumingWebSocket + .Verify( + x => x.ConnectAsync( + It.Is(uri => uri.Equals(expectedUri)), + It.IsAny() + ), + Times.Once + ); + } + + [Theory] + [InlineData(true, 1)] + [InlineData(false, 0)] + public async Task ConnectAsync_OnceConnectedWhenInvalidSessionEventIsReceived_ItShouldResumeOrReconnectCorrectly( + bool canResume, + int resumeCount + ) + { + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var initialWebSocketState = WebSocketState.Closed; + + var initialWebSocket = new Mock(); + + initialWebSocket + .Setup(static x => x.State) + .Returns(() => initialWebSocketState); + + initialWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + initialWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => initialWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var resumeWebSocketState = WebSocketState.Closed; + + var resumingWebSocket = new Mock(); + + resumingWebSocket + .Setup(static x => x.State) + .Returns(() => resumeWebSocketState); + + resumingWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + resumingWebSocket + .Setup(static x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => resumeWebSocketState = WebSocketState.Closed) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + var invalidSessionEvent = new InvalidSessionDiscordEvent() + { + Data = canResume, + }; + var invalidSessionPayload = CreateEventPayload(invalidSessionEvent); + var invalidSessionResult = new WebSocketReceiveResult( + invalidSessionPayload.Bytes.Length, + WebSocketMessageType.Text, + true + ); + messagesToReceive.Enqueue((invalidSessionResult, invalidSessionPayload.Bytes)); + + SetupReceiveMessageSequence(initialWebSocket, messagesToReceive); + + _mockWebSocketFactory + .SetupSequence(static x => x.Create()) + .Returns(initialWebSocket.Object) + .Returns(resumingWebSocket.Object); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + await cts.CancelAsync(); + + var expectedUri = new Uri($"{resumeGatewayUrl}/?v=10&encoding=json"); + + resumingWebSocket + .Verify( + x => x.ConnectAsync( + It.Is(uri => uri.Equals(expectedUri)), + It.IsAny() + ), + Times.Exactly(resumeCount) + ); } [Fact] - public Task ConnectAsync_OnceConnectedWhenResumableCloseStatusIsReceived_ItShouldResume() + public async Task DisconnectAsync_WhenCalled_ItShouldDisconnectAndSendIdleStatus() { - return Task.CompletedTask; + _mockDiscordRestClient + .Setup(static x => x.GetGatewayUrlAsync(It.IsAny())) + .ReturnsAsync("wss://gateway.discord.gg"); + + var socketState = WebSocketState.Closed; + + var mockWebSocket = new Mock(); + + mockWebSocket + .Setup(static x => x.State) + .Returns(() => socketState); + + mockWebSocket + .Setup(static x => x.ConnectAsync(It.IsAny(), It.IsAny())) + .Callback(() => socketState = WebSocketState.Open) + .Returns(Task.CompletedTask); + + var messagesToReceive = new Queue<(WebSocketReceiveResult, byte[])>(); + var heartbeatInterval = 100; + var helloEvent = new HelloDiscordEvent() + { + Data = new() + { + HeartbeatInterval = heartbeatInterval, + } + }; + var helloPayload = CreateEventPayload(helloEvent); + var helloResult = new WebSocketReceiveResult(helloPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((helloResult, helloPayload.Bytes)); + + var sessionId = "session_id"; + var resumeGatewayUrl = "wss://resume.discord.gg"; + var readyEvent = new ReadyDiscordEvent() + { + Data = new ReadyData() + { + SessionId = sessionId, + ResumeGatewayUrl = resumeGatewayUrl, + } + }; + var readyPayload = CreateEventPayload(readyEvent); + var readyResult = new WebSocketReceiveResult(readyPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((readyResult, readyPayload.Bytes)); + + var heartbeatAckEvent = new HeartbeatAckDiscordEvent(); + var heartbeatAckPayload = CreateEventPayload(heartbeatAckEvent); + var heartbeatAckResult = new WebSocketReceiveResult(heartbeatAckPayload.Bytes.Length, WebSocketMessageType.Text, true); + messagesToReceive.Enqueue((heartbeatAckResult, heartbeatAckPayload.Bytes)); + + SetupReceiveMessageSequence(mockWebSocket, messagesToReceive); + + _mockWebSocketFactory + .Setup(static x => x.Create()) + .Returns(mockWebSocket.Object); + + var now = DateTime.UtcNow; + + _mockTimeProvider + .Setup(static x => x.GetUtcNow()) + .Returns(now); + + using var cts = new CancellationTokenSource(); + + await _discordGatewayClient.ConnectAsync(cts.Token); + await Task.Delay(heartbeatInterval * 2); + + await _discordGatewayClient.DisconnectAsync(cts.Token); + + var expectedIdleEvent = new UpdatePresenceDiscordEvent( + now.Millisecond, + [ + new Activity + { + Name = "Taking a break. Stevan's got this.", + Type = ActivityType.Custom, + State = "Taking a break. Stevan's got this.", + } + ], + PresenceStatus.Idle, + true + ); + var expectedIdlePayload = CreateEventPayload(expectedIdleEvent); + + mockWebSocket + .Verify( + x => x.SendAsync( + It.Is>(b => expectedIdlePayload.Bytes.SequenceEqual(b.Array!)), + It.Is(m => m == WebSocketMessageType.Text), + true, + It.IsAny() + ), + Times.Once + ); + + mockWebSocket + .Verify( + static x => x.CloseAsync(WebSocketCloseStatus.NormalClosure, It.IsAny(), It.IsAny()), + Times.Once + ); } [Fact] - public Task ConnectAsync_OnceConnectedWhenReconnectEventIsReceivedThatIndicatesClientCanResume_ItShouldResume() + public void On_WhenCalledWithInvalidEvent_ItShouldThrowException() { - return Task.CompletedTask; + var handler = new Func((e, sp, ct) => Task.CompletedTask); + var act = () => _discordGatewayClient.On("made_it_up", handler); + act.Should().Throw(); } [Fact] - public Task ConnectAsync_OnceConnectedWhenReconnectEventIsReceivedThatIndicatesClientCannotResume_ItShouldReconnect() + public void On_WhenCalledWithNullHandler_ItShouldThrowException() { - return Task.CompletedTask; + var act = () => _discordGatewayClient.On(DiscordEventTypes.Ready, null!); + act.Should().Throw(); + } + + [Fact] + public void Off_WhenCalledWithInvalidEvent_ItShouldThrowException() + { + var handler = new Func((e, sp, ct) => Task.CompletedTask); + var act = () => _discordGatewayClient.Off("made_it_up", handler); + act.Should().Throw(); + } + + [Fact] + public void Off_WhenCalledWithNullHandler_ItShouldThrowException() + { + var act = () => _discordGatewayClient.Off(DiscordEventTypes.Ready, null!); + act.Should().Throw(); } private static void SetupReceiveMessageSequence( diff --git a/src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs b/src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs index 3dc4c22..195563f 100644 --- a/src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs +++ b/src/StevesBot.Worker.Tests/Unit/InvalidSessionDiscordEventTests.cs @@ -7,7 +7,7 @@ public class InvalidSessionDiscordEventTests { var result = new InvalidSessionDiscordEvent(); - result.OpCode.Should().Be(0); + result.OpCode.Should().Be(DiscordOpCodes.InvalidSession); result.Type.Should().BeNull(); result.Sequence.Should().BeNull(); result.Data.Should().BeFalse(); diff --git a/src/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs b/src/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs new file mode 100644 index 0000000..39182a3 --- /dev/null +++ b/src/StevesBot.Worker.Tests/Unit/ReconnectDiscordEventTests.cs @@ -0,0 +1,15 @@ +namespace StevesBot.Worker.Tests.Unit; + +public class ReconnectDiscordEventTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstance() + { + var result = new ReconnectDiscordEvent(); + + result.OpCode.Should().Be(DiscordOpCodes.Reconnect); + result.Sequence.Should().BeNull(); + result.Type.Should().BeNull(); + result.Data.Should().BeNull(); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs index dc14cea..1578fc3 100644 --- a/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs @@ -4,4 +4,9 @@ internal sealed record InvalidSessionDiscordEvent : DiscordEvent { [JsonPropertyName("d")] public new bool Data { get; init; } + + public InvalidSessionDiscordEvent() + { + OpCode = DiscordOpCodes.InvalidSession; + } } \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs index 3314c5b..9287511 100644 --- a/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs @@ -2,4 +2,8 @@ namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record ReconnectDiscordEvent : DiscordEvent { + public ReconnectDiscordEvent() + { + OpCode = DiscordOpCodes.Reconnect; + } } \ No newline at end of file