diff --git a/src/StevesBot.Worker/Discord/Events/MessageCreateDiscordEvent.cs b/src/StevesBot.Worker/Discord/Events/MessageCreateDiscordEvent.cs deleted file mode 100644 index 50fa219..0000000 --- a/src/StevesBot.Worker/Discord/Events/MessageCreateDiscordEvent.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace StevesBot.Worker.Discord.Events; - -internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent -{ - [JsonPropertyName("d")] - public new MessageCreateData Data { get; init; } = new MessageCreateData(); - - public bool IsMessageType(int messageType) - { - return Data.Type == messageType; - } -} - -internal sealed record MessageCreateData -{ - [JsonPropertyName("id")] - public string Id { get; init; } = string.Empty; - - [JsonPropertyName("type")] - public int Type { get; init; } - - [JsonPropertyName("channel_id")] - public string ChannelId { get; init; } = string.Empty; - - [JsonPropertyName("guild_id")] - public string GuildId { get; init; } = string.Empty; - - [JsonPropertyName("author")] - public DiscordUser Author { get; init; } = new DiscordUser(); - -} - -internal sealed record DiscordUser -{ - [JsonPropertyName("id")] - public string Id { get; init; } = string.Empty; -} diff --git a/src/StevesBot.Worker/Discord/Extensions.cs b/src/StevesBot.Worker/Discord/Extensions.cs new file mode 100644 index 0000000..a1eccf2 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Extensions.cs @@ -0,0 +1,50 @@ +namespace StevesBot.Worker.Discord; + +internal static class Extensions +{ + public static IServiceCollection AddDiscordRestClient(this IServiceCollection services) + { + services + .AddHttpClient(static (sp, c) => + { + var discordOptions = sp.GetRequiredService(); + c.BaseAddress = new Uri(discordOptions.ApiUrl); + c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); + c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)"); + }) + .AddStandardResilienceHandler(); + + return services; + } + + public static IServiceCollection AddDiscordGatewayClient(this IServiceCollection services, Action? configure) + { + services.AddSingleton(sp => + { + var discordOptions = sp.GetRequiredService(); + var discordRestClient = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var socketFactory = sp.GetRequiredService(); + var timeProvider = sp.GetRequiredService(); + var serviceScopeFactory = sp.GetRequiredService(); + + var client = new DiscordGatewayClient( + discordOptions, + socketFactory, + logger, + discordRestClient, + timeProvider, + serviceScopeFactory + ); + + if (configure is not null) + { + configure(client); + } + + return client; + }); + + return services; + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/DiscordCloseCodes.cs b/src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs similarity index 92% rename from src/StevesBot.Worker/Discord/DiscordCloseCodes.cs rename to src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs index 60bc62f..12323ab 100644 --- a/src/StevesBot.Worker/Discord/DiscordCloseCodes.cs +++ b/src/StevesBot.Worker/Discord/Gateway/DiscordCloseCodes.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway; internal static class DiscordCloseCodes { diff --git a/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs similarity index 91% rename from src/StevesBot.Worker/Discord/DiscordGatewayClient.cs rename to src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs index 1d17649..71c3c66 100644 --- a/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs +++ b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs @@ -1,6 +1,4 @@ -using System.Text; - -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway; internal sealed class DiscordGatewayClient : IDiscordGatewayClient { @@ -129,8 +127,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken) { - await CancelReceiveMessagesTaskAsync(cancellationToken); - var newReceiveCts = new CancellationTokenSource(); var newReceiveLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newReceiveCts.Token); @@ -164,10 +160,8 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient } var canResume = IsResumableCloseCode(result.CloseStatus); - var closeStatus = canResume ? WebSocketCloseStatus.MandatoryExtension : WebSocketCloseStatus.NormalClosure; await SetCanResumeAsync(canResume, _linkedReceiveMessageCts.Token); - await CloseIfOpenAsync(closeStatus, result.CloseStatusDescription, _linkedReceiveMessageCts.Token); _logger.LogWarning("Reconnecting because of close message: {CloseStatus} - {CloseStatusDescription}", result.CloseStatus, result.CloseStatusDescription); @@ -213,12 +207,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient _logger.LogWarning("Closing connection and invalidating session."); - await CloseIfOpenAsync( - WebSocketCloseStatus.NormalClosure, - "WebSocket error. Closing connection and invalidating session.", - newReceiveLinkedCts.Token - ); - await SetCanResumeAsync(false, newReceiveLinkedCts.Token); _logger.LogWarning("Reconnecting because of error in receive message task"); @@ -293,12 +281,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient await SetCanResumeAsync(true, cancellationToken); - await CloseIfOpenAsync( - WebSocketCloseStatus.MandatoryExtension, - "Reconnect event received", - cancellationToken - ); - _logger.LogWarning("Reconnecting because of reconnect event"); await ReconnectAsync(cancellationToken); @@ -311,24 +293,12 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient _logger.LogWarning("Session is resumable. Closing connection without invalidating session."); await SetCanResumeAsync(true, cancellationToken); - - await CloseIfOpenAsync( - WebSocketCloseStatus.MandatoryExtension, - "Invalid session event received", - cancellationToken - ); } else { _logger.LogWarning("Session is not resumable. Closing connection and invalidating session."); await SetCanResumeAsync(false, cancellationToken); - - await CloseIfOpenAsync( - WebSocketCloseStatus.NormalClosure, - "Invalid session event received", - cancellationToken - ); } await ReconnectAsync(cancellationToken); @@ -341,8 +311,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient private async Task StartHeartbeatAsync(CancellationToken cancellationToken) { - await CancelHeartbeatTaskAsync(cancellationToken); - var newHeartbeatCts = new CancellationTokenSource(); var newLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newHeartbeatCts.Token); @@ -365,14 +333,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient { if (IsWebSocketOpen()) { - _logger.LogWarning("Heartbeat not acknowledged. Closing WebSocket."); - - await CloseAsync( - WebSocketCloseStatus.ProtocolError, - "Heartbeat not acknowledged", - _linkedHeartbeatCts.Token - ); - await SetCanResumeAsync(true, _linkedHeartbeatCts.Token); } @@ -397,14 +357,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient { _logger.LogError(ex, "Error in heartbeat task: {Message}", ex.Message); - _logger.LogWarning("Closing connection and invalidating session."); - - await CloseIfOpenAsync( - WebSocketCloseStatus.NormalClosure, - "Heartbeat error. Closing connection and invalidating session.", - newLinkedCts.Token - ); - _logger.LogWarning("Reconnecting because of error in heartbeat task"); await ReconnectAsync(cancellationToken); @@ -415,6 +367,17 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient private async Task ReconnectAsync(CancellationToken cancellationToken) { + await CancelReceiveMessagesTaskAsync(cancellationToken); + await CancelHeartbeatTaskAsync(cancellationToken); + + var closeStatus = _canResume ? WebSocketCloseStatus.MandatoryExtension : WebSocketCloseStatus.NormalClosure; + + await CloseIfOpenAsync( + closeStatus, + "Reconnecting", + cancellationToken + ); + _webSocket?.Dispose(); if (_canResume) @@ -638,21 +601,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient await _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); } - private async Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken) - { - if (_webSocket is null) - { - throw new DiscordGatewayClientException("WebSocket is not set. Cannot close."); - } - - if (_webSocket.State is not WebSocketState.Open) - { - throw new DiscordGatewayClientException("WebSocket is not open. Cannot close."); - } - - await _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); - } - private async Task ReceiveMessageAsync( ArraySegment segment, CancellationToken cancellationToken diff --git a/src/StevesBot.Worker/Discord/DiscordGatewayClientException.cs b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs similarity index 87% rename from src/StevesBot.Worker/Discord/DiscordGatewayClientException.cs rename to src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs index e90cc1f..d58cb19 100644 --- a/src/StevesBot.Worker/Discord/DiscordGatewayClientException.cs +++ b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClientException.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway; internal sealed class DiscordGatewayClientException : Exception { diff --git a/src/StevesBot.Worker/Discord/DiscordIntents.cs b/src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs similarity index 97% rename from src/StevesBot.Worker/Discord/DiscordIntents.cs rename to src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs index 2d58e15..ac40ee4 100644 --- a/src/StevesBot.Worker/Discord/DiscordIntents.cs +++ b/src/StevesBot.Worker/Discord/Gateway/DiscordIntents.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway; internal static class DiscordIntents { diff --git a/src/StevesBot.Worker/Discord/Events/DiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs similarity index 84% rename from src/StevesBot.Worker/Discord/Events/DiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs index 749bccb..27c77e9 100644 --- a/src/StevesBot.Worker/Discord/Events/DiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal record DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/DiscordEventConverter.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs similarity index 97% rename from src/StevesBot.Worker/Discord/Events/DiscordEventConverter.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs index 402d790..2e7651d 100644 --- a/src/StevesBot.Worker/Discord/Events/DiscordEventConverter.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventConverter.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed class DiscordEventConverter : JsonConverter { diff --git a/src/StevesBot.Worker/Discord/Events/DiscordEventTypes.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs similarity index 88% rename from src/StevesBot.Worker/Discord/Events/DiscordEventTypes.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs index db4cf04..1553b5a 100644 --- a/src/StevesBot.Worker/Discord/Events/DiscordEventTypes.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordEventTypes.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal static class DiscordEventTypes { diff --git a/src/StevesBot.Worker/Discord/Events/DiscordMessageTypes.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs similarity index 60% rename from src/StevesBot.Worker/Discord/Events/DiscordMessageTypes.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs index 5843995..446078d 100644 --- a/src/StevesBot.Worker/Discord/Events/DiscordMessageTypes.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordMessageTypes.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal static class DiscordMessageTypes { diff --git a/src/StevesBot.Worker/Discord/Events/DiscordOpCodes.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs similarity index 87% rename from src/StevesBot.Worker/Discord/Events/DiscordOpCodes.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs index 0b471a4..7d9aaeb 100644 --- a/src/StevesBot.Worker/Discord/Events/DiscordOpCodes.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DiscordOpCodes.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal static class DiscordOpCodes { diff --git a/src/StevesBot.Worker/Discord/Events/DispatchDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs similarity index 52% rename from src/StevesBot.Worker/Discord/Events/DispatchDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs index f26b539..bcf4c82 100644 --- a/src/StevesBot.Worker/Discord/Events/DispatchDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/DispatchDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal record DispatchDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/HeartbeatAckDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs similarity index 56% rename from src/StevesBot.Worker/Discord/Events/HeartbeatAckDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs index 53cb764..e005a88 100644 --- a/src/StevesBot.Worker/Discord/Events/HeartbeatAckDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatAckDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record HeartbeatAckDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/HeartbeatDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs similarity index 78% rename from src/StevesBot.Worker/Discord/Events/HeartbeatDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs index 874d652..c04048d 100644 --- a/src/StevesBot.Worker/Discord/Events/HeartbeatDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/HeartbeatDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record HeartbeatDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/HelloDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs similarity index 84% rename from src/StevesBot.Worker/Discord/Events/HelloDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs index 3745b91..16569ef 100644 --- a/src/StevesBot.Worker/Discord/Events/HelloDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/HelloDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record HelloDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/IdentifyDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs similarity index 95% rename from src/StevesBot.Worker/Discord/Events/IdentifyDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs index afd6d3d..267a77d 100644 --- a/src/StevesBot.Worker/Discord/Events/IdentifyDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/IdentifyDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record IdentifyDiscordEvent : DiscordEvent { @@ -42,4 +42,4 @@ internal sealed record IdentifyProperties [JsonPropertyName("device")] public string Device { get; init; } = Assembly.GetExecutingAssembly().GetName().FullName; -} +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Events/InvalidSessionDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs similarity index 72% rename from src/StevesBot.Worker/Discord/Events/InvalidSessionDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs index a6b951e..dc14cea 100644 --- a/src/StevesBot.Worker/Discord/Events/InvalidSessionDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/InvalidSessionDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record InvalidSessionDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs new file mode 100644 index 0000000..6bb2bf8 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Gateway/Events/MessageCreateDiscordEvent.cs @@ -0,0 +1,12 @@ +namespace StevesBot.Worker.Discord.Gateway.Events; + +internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent +{ + [JsonPropertyName("d")] + public new DiscordMessage Data { get; init; } = new DiscordMessage(); + + public bool IsMessageType(int messageType) + { + return Data.Type == messageType; + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Events/ReadyDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs similarity index 89% rename from src/StevesBot.Worker/Discord/Events/ReadyDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs index 0a1a5fc..dfa0645 100644 --- a/src/StevesBot.Worker/Discord/Events/ReadyDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/ReadyDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record ReadyDiscordEvent : DispatchDiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/ReconnectDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs similarity index 55% rename from src/StevesBot.Worker/Discord/Events/ReconnectDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs index a44d4e6..3314c5b 100644 --- a/src/StevesBot.Worker/Discord/Events/ReconnectDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/ReconnectDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record ReconnectDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/ResumeDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs similarity index 92% rename from src/StevesBot.Worker/Discord/Events/ResumeDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs index 00bd78c..2c23dc8 100644 --- a/src/StevesBot.Worker/Discord/Events/ResumeDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/ResumeDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record ResumeDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/Events/UpdatePresenceDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs similarity index 96% rename from src/StevesBot.Worker/Discord/Events/UpdatePresenceDiscordEvent.cs rename to src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs index 3170b1f..b64af13 100644 --- a/src/StevesBot.Worker/Discord/Events/UpdatePresenceDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord.Events; +namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record UpdatePresenceDiscordEvent : DiscordEvent { diff --git a/src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs b/src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs similarity index 89% rename from src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs rename to src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs index 4eb283f..d4a9c69 100644 --- a/src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs +++ b/src/StevesBot.Worker/Discord/Gateway/IDiscordGatewayClient.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Gateway; internal interface IDiscordGatewayClient : IDisposable { diff --git a/src/StevesBot.Worker/Discord/IDiscordRestClient.cs b/src/StevesBot.Worker/Discord/IDiscordRestClient.cs deleted file mode 100644 index 0b3a539..0000000 --- a/src/StevesBot.Worker/Discord/IDiscordRestClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StevesBot.Worker.Discord; - -internal interface IDiscordRestClient -{ - Task GetGatewayUrlAsync(CancellationToken cancellationToken); - Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/DiscordRestClient.cs b/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs similarity index 62% rename from src/StevesBot.Worker/Discord/DiscordRestClient.cs rename to src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs index 10f803d..c3c8383 100644 --- a/src/StevesBot.Worker/Discord/DiscordRestClient.cs +++ b/src/StevesBot.Worker/Discord/Rest/DiscordRestClient.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Rest; internal sealed class DiscordRestClient : IDiscordRestClient { @@ -14,7 +14,6 @@ internal sealed class DiscordRestClient : IDiscordRestClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } - public async Task GetGatewayUrlAsync(CancellationToken cancellationToken) { var gatewayEndpoint = new Uri("gateway", UriKind.Relative); @@ -37,7 +36,7 @@ internal sealed class DiscordRestClient : IDiscordRestClient return gatewayResponse.Url; } - public async Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken) + public async Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken) { var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative); var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken); @@ -48,37 +47,14 @@ internal sealed class DiscordRestClient : IDiscordRestClient throw new DiscordRestClientException("Failed to create message."); } - var messageCreateData = await response.Content.ReadFromJsonAsync(cancellationToken); + var discordMessage = await response.Content.ReadFromJsonAsync(cancellationToken); - if (messageCreateData is null) + if (discordMessage is null) { _logger.LogError("Failed to deserialize message create response."); throw new DiscordRestClientException("Failed to deserialize message create response."); } - return messageCreateData; + return discordMessage; } } - -internal sealed record GatewayResponse( - [property: JsonPropertyName("url")] string Url -); - -internal sealed record CreateMessageRequest( - [property: JsonPropertyName("content")] string Content, - [property: JsonPropertyName("message_reference")] MessageReference? MessageReference -); - -internal sealed record MessageReference( - [property: JsonPropertyName("type")] int Type, - [property: JsonPropertyName("message_id")] string MessageId, - [property: JsonPropertyName("channel_id")] string ChannelId, - [property: JsonPropertyName("guild_id")] string GuildId, - [property: JsonPropertyName("fail_if_not_exists")] bool FailIfNotExists -); - -internal static class MessageReferenceTypes -{ - public const int Default = 0; - public const int Forward = 1; -} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/DiscordRestClientException.cs b/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs similarity index 88% rename from src/StevesBot.Worker/Discord/DiscordRestClientException.cs rename to src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs index 70b5fb3..9e8e4b5 100644 --- a/src/StevesBot.Worker/Discord/DiscordRestClientException.cs +++ b/src/StevesBot.Worker/Discord/Rest/DiscordRestClientException.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Rest; internal sealed class DiscordRestClientException : Exception { diff --git a/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs b/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs new file mode 100644 index 0000000..e9a8e8d --- /dev/null +++ b/src/StevesBot.Worker/Discord/Rest/IDiscordRestClient.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Worker.Discord.Rest; + +internal interface IDiscordRestClient +{ + Task GetGatewayUrlAsync(CancellationToken cancellationToken); + Task CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs b/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs new file mode 100644 index 0000000..8203c18 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Rest/Requests/CreateMessageRequest.cs @@ -0,0 +1,6 @@ +namespace StevesBot.Worker.Discord.Rest.Requests; + +internal sealed record CreateMessageRequest( + [property: JsonPropertyName("content")] string Content, + [property: JsonPropertyName("message_reference")] MessageReference? MessageReference +); \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs b/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs new file mode 100644 index 0000000..e8f7a69 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Rest/Responses/GatewayResponse.cs @@ -0,0 +1,5 @@ +namespace StevesBot.Worker.Discord.Rest.Responses; + +internal sealed record GatewayResponse( + [property: JsonPropertyName("url")] string Url +); \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/DiscordClientOptions.cs b/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs similarity index 81% rename from src/StevesBot.Worker/Discord/DiscordClientOptions.cs rename to src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs index 3d3d11f..38547a0 100644 --- a/src/StevesBot.Worker/Discord/DiscordClientOptions.cs +++ b/src/StevesBot.Worker/Discord/Shared/DiscordClientOptions.cs @@ -1,4 +1,4 @@ -namespace StevesBot.Worker.Discord; +namespace StevesBot.Worker.Discord.Shared; internal sealed class DiscordClientOptions { diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs b/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs new file mode 100644 index 0000000..e29088f --- /dev/null +++ b/src/StevesBot.Worker/Discord/Shared/DiscordMessage.cs @@ -0,0 +1,19 @@ +namespace StevesBot.Worker.Discord.Shared; + +internal sealed record DiscordMessage +{ + [JsonPropertyName("id")] + public string Id { get; init; } = string.Empty; + + [JsonPropertyName("type")] + public int Type { get; init; } + + [JsonPropertyName("channel_id")] + public string ChannelId { get; init; } = string.Empty; + + [JsonPropertyName("guild_id")] + public string GuildId { get; init; } = string.Empty; + + [JsonPropertyName("author")] + public DiscordUser Author { get; init; } = new DiscordUser(); +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs new file mode 100644 index 0000000..f911493 --- /dev/null +++ b/src/StevesBot.Worker/Discord/Shared/DiscordMessageReference.cs @@ -0,0 +1,15 @@ +namespace StevesBot.Worker.Discord.Shared; + +internal sealed record MessageReference( + [property: JsonPropertyName("type")] int Type, + [property: JsonPropertyName("message_id")] string MessageId, + [property: JsonPropertyName("channel_id")] string ChannelId, + [property: JsonPropertyName("guild_id")] string GuildId, + [property: JsonPropertyName("fail_if_not_exists")] bool FailIfNotExists +); + +internal static class MessageReferenceTypes +{ + public const int Default = 0; + public const int Forward = 1; +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs b/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs new file mode 100644 index 0000000..eba9acf --- /dev/null +++ b/src/StevesBot.Worker/Discord/Shared/DiscordUser.cs @@ -0,0 +1,7 @@ +namespace StevesBot.Worker.Discord.Shared; + +internal sealed record DiscordUser +{ + [JsonPropertyName("id")] + public string Id { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs b/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs new file mode 100644 index 0000000..9cb5791 --- /dev/null +++ b/src/StevesBot.Worker/Handlers/WelcomeMessageHandler.cs @@ -0,0 +1,53 @@ +using System.Globalization; +using System.Text; + +namespace StevesBot.Worker.Handlers; + +internal static class WelcomeMessageHandler +{ + public static async Task HandleAsync( + DiscordEvent discordEvent, + IServiceProvider serviceProvider, + CancellationToken cancellationToken + ) + { + var discordRestClient = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false) + { + return; + } + + logger.LogInformation("Received user join message for user: {UserId}", mcde.Data.Author.Id); + var welcomeMessage = GetWelcomeMessage(mcde.Data.Author.Id); + + var request = new CreateMessageRequest( + Content: welcomeMessage, + MessageReference: new( + Type: MessageReferenceTypes.Default, + MessageId: mcde.Data.Id, + ChannelId: mcde.Data.ChannelId, + GuildId: mcde.Data.GuildId, + FailIfNotExists: false + ) + ); + + var message = await discordRestClient.CreateMessageAsync(mcde.Data.ChannelId, request, cancellationToken); + + logger.LogInformation("Created welcome message with Id: {MessageId} for user: {UserId}", message.Id, mcde.Data.Author.Id); + } + + private static string GetWelcomeMessage(string userId) + { + var builder = new StringBuilder(); + + builder.AppendLine(CultureInfo.InvariantCulture, $"Hi there <@{userId}>! 👋🏻 Welcome to Stevan's server."); + builder.AppendLine(); + builder.AppendLine("We're so stoked to have you join our community here. Feel free to jump right in, tell us a bit about yourself, and explore all the different channels."); + builder.AppendLine(); + builder.AppendLine("If you have any questions at all, don't hesitate to ask. We're a friendly bunch and always happy to help out. Glad you're here!"); + + return builder.ToString(); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Program.cs b/src/StevesBot.Worker/Program.cs index 39920b2..5b8ecd0 100644 --- a/src/StevesBot.Worker/Program.cs +++ b/src/StevesBot.Worker/Program.cs @@ -1,7 +1,3 @@ -using System.Net.Http.Headers; - -using Microsoft.Extensions.Options; - var builder = Host.CreateApplicationBuilder(args); builder.Services.Configure(static options => options.ShutdownTimeout = TimeSpan.FromSeconds(30)); @@ -18,20 +14,11 @@ builder.Services.AddSingleton(static sp => builder.Services.AddSingleton(); builder.Services.AddSingleton(TimeProvider.System); -builder.Services - .AddHttpClient(static (sp, c) => - { - var discordOptions = sp.GetRequiredService(); - c.BaseAddress = new Uri(discordOptions.ApiUrl); - c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); - c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)"); - }) - .AddStandardResilienceHandler(); +builder.Services.AddDiscordRestClient(); -// TODO: Create an extension method that allows adding -// the discord gateway client and allows me to configure -// event handlers -builder.Services.AddSingleton(); +builder.Services.AddDiscordGatewayClient(static (client) => + client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync) +); builder.Services.AddHostedService(); diff --git a/src/StevesBot.Worker/Usings.cs b/src/StevesBot.Worker/Usings.cs index 4a67de3..347427c 100644 --- a/src/StevesBot.Worker/Usings.cs +++ b/src/StevesBot.Worker/Usings.cs @@ -1,12 +1,20 @@ global using System.Net.Http.Json; global using System.Net.WebSockets; global using System.Reflection; +global using System.Text; global using System.Text.Json; global using System.Text.Json.Serialization; -global using StevesBot.Worker.Discord.Events; -global using StevesBot.Worker.Threading; -global using StevesBot.Worker.WebSockets; +global using Microsoft.Extensions.Options; global using StevesBot.Worker; -global using StevesBot.Worker.Discord; \ No newline at end of file +global using StevesBot.Worker.Discord; +global using StevesBot.Worker.Discord.Gateway; +global using StevesBot.Worker.Discord.Gateway.Events; +global using StevesBot.Worker.Discord.Rest; +global using StevesBot.Worker.Discord.Rest.Requests; +global using StevesBot.Worker.Discord.Rest.Responses; +global using StevesBot.Worker.Discord.Shared; +global using StevesBot.Worker.Handlers; +global using StevesBot.Worker.Threading; +global using StevesBot.Worker.WebSockets; \ No newline at end of file diff --git a/src/StevesBot.Worker/Worker.cs b/src/StevesBot.Worker/Worker.cs index b1a3cef..cb3496b 100644 --- a/src/StevesBot.Worker/Worker.cs +++ b/src/StevesBot.Worker/Worker.cs @@ -15,35 +15,6 @@ internal class Worker : IHostedService public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Connecting Discord Gateway Client"); - - _discordGatewayClient.On(DiscordEventTypes.MessageCreate, static async (discordEvent, sp, cancellationToken) => - { - var discordRestClient = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - - if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false) - { - return; - } - - logger.LogInformation("Received user join message for user: {UserId}", mcde.Data.Author.Id); - - var request = new CreateMessageRequest( - Content: $"Welcome to the server <@{mcde.Data.Author.Id}>! We're glad to have you here.", - MessageReference: new( - Type: MessageReferenceTypes.Default, - MessageId: mcde.Data.Id, - ChannelId: mcde.Data.ChannelId, - GuildId: mcde.Data.GuildId, - FailIfNotExists: false - ) - ); - - var message = await discordRestClient.CreateMessageAsync(mcde.Data.ChannelId, request, cancellationToken); - - logger.LogInformation("Created welcome message with Id: {MessageId} for user: {UserId}", message.Id, mcde.Data.Author.Id); - }); - return _discordGatewayClient.ConnectAsync(cancellationToken); }