From db221334cac378d0a1bc99ecb79bfb75fd0c1e33 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 May 2025 15:54:00 -0500 Subject: [PATCH] feat: implement sending welcome message --- .../Discord/DiscordGatewayClient.cs | 8 +-- .../Discord/DiscordRestClient.cs | 49 +++++++++++++++++-- .../Discord/IDiscordGatewayClient.cs | 4 +- .../Discord/IDiscordRestClient.cs | 1 + src/StevesBot.Worker/Program.cs | 3 ++ src/StevesBot.Worker/Worker.cs | 40 +++++++-------- 6 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs b/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs index a42b6e4..1d17649 100644 --- a/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs +++ b/src/StevesBot.Worker/Discord/DiscordGatewayClient.cs @@ -25,7 +25,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient }, }; private readonly AsyncLock _lock = new(); - private readonly Dictionary> _eventHandlers = []; + private readonly Dictionary> _eventHandlers = []; private string _gatewayUrl = string.Empty; private IWebSocket? _webSocket; @@ -85,7 +85,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient await CloseIfOpenAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", cancellationToken); } - public void On(string eventName, Func handler) + public void On(string eventName, Func handler) { if (DiscordEventTypes.IsValidEvent(eventName) is false) { @@ -103,7 +103,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient _eventHandlers[eventName] += handler; } - public void Off(string eventName, Func handler) + public void Off(string eventName, Func handler) { if (DiscordEventTypes.IsValidEvent(eventName) is false) { @@ -271,7 +271,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient { await using var scope = _serviceScopeFactory.CreateAsyncScope(); // TODO: Pass cancellation token to handler - await handler(de, scope.ServiceProvider); + await handler(de, scope.ServiceProvider, cancellationToken); } # pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) diff --git a/src/StevesBot.Worker/Discord/DiscordRestClient.cs b/src/StevesBot.Worker/Discord/DiscordRestClient.cs index 76db332..10f803d 100644 --- a/src/StevesBot.Worker/Discord/DiscordRestClient.cs +++ b/src/StevesBot.Worker/Discord/DiscordRestClient.cs @@ -17,10 +17,10 @@ internal sealed class DiscordRestClient : IDiscordRestClient public async Task GetGatewayUrlAsync(CancellationToken cancellationToken) { - var uri = new Uri("gateway", UriKind.Relative); - var response = await _httpClient.GetAsync(uri, cancellationToken); + var gatewayEndpoint = new Uri("gateway", UriKind.Relative); + var response = await _httpClient.GetAsync(gatewayEndpoint, cancellationToken); - if (!response.IsSuccessStatusCode) + if (response.IsSuccessStatusCode is false) { _logger.LogError("Failed to get gateway URL: {StatusCode}", response.StatusCode); throw new DiscordRestClientException("Failed to get gateway URL."); @@ -36,8 +36,49 @@ internal sealed class DiscordRestClient : IDiscordRestClient return gatewayResponse.Url; } + + 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); + + if (response.IsSuccessStatusCode is false) + { + _logger.LogError("Failed to create message: {StatusCode}", response.StatusCode); + throw new DiscordRestClientException("Failed to create message."); + } + + var messageCreateData = await response.Content.ReadFromJsonAsync(cancellationToken); + + if (messageCreateData is null) + { + _logger.LogError("Failed to deserialize message create response."); + throw new DiscordRestClientException("Failed to deserialize message create response."); + } + + return messageCreateData; + } } -internal record GatewayResponse( +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/IDiscordGatewayClient.cs b/src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs index 2127ac0..4eb283f 100644 --- a/src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs +++ b/src/StevesBot.Worker/Discord/IDiscordGatewayClient.cs @@ -4,6 +4,6 @@ internal interface IDiscordGatewayClient : IDisposable { Task ConnectAsync(CancellationToken cancellationToken); Task DisconnectAsync(CancellationToken cancellationToken); - void On(string eventName, Func handler); - void Off(string eventName, Func handler); + void On(string eventName, Func handler); + void Off(string eventName, Func handler); } \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/IDiscordRestClient.cs b/src/StevesBot.Worker/Discord/IDiscordRestClient.cs index 4a46f09..0b3a539 100644 --- a/src/StevesBot.Worker/Discord/IDiscordRestClient.cs +++ b/src/StevesBot.Worker/Discord/IDiscordRestClient.cs @@ -3,4 +3,5 @@ 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/Program.cs b/src/StevesBot.Worker/Program.cs index 054364d..39920b2 100644 --- a/src/StevesBot.Worker/Program.cs +++ b/src/StevesBot.Worker/Program.cs @@ -1,3 +1,5 @@ +using System.Net.Http.Headers; + using Microsoft.Extensions.Options; var builder = Host.CreateApplicationBuilder(args); @@ -22,6 +24,7 @@ builder.Services 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(); diff --git a/src/StevesBot.Worker/Worker.cs b/src/StevesBot.Worker/Worker.cs index 7ddb561..b1a3cef 100644 --- a/src/StevesBot.Worker/Worker.cs +++ b/src/StevesBot.Worker/Worker.cs @@ -16,34 +16,32 @@ internal class Worker : IHostedService { _logger.LogInformation("Connecting Discord Gateway Client"); - // TODO: Provide access to the gateway client - // in delegate...don't try to resolve it from the service provider - // TODO: Provide .On and .Off method overloads to allow - // caller to not need to use discard for unused parameters - _discordGatewayClient.On(DiscordEventTypes.MessageCreate, static (discordEvent, sp) => + _discordGatewayClient.On(DiscordEventTypes.MessageCreate, static async (discordEvent, sp, cancellationToken) => { var discordRestClient = sp.GetRequiredService(); var logger = sp.GetRequiredService>(); - // TODO: When a user join message is received, - // we are going to reply to the message with - // a welcome greeting - // A user join message is type 8 - // Will need to use REST API to reply to the message - - if ( - discordEvent is MessageCreateDiscordEvent mcde && - mcde.IsMessageType(DiscordMessageTypes.UserJoin) - ) + if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false) { - logger.LogInformation("Guild Id: {GuildId}", mcde.Data.GuildId); - logger.LogInformation("Channel Id: {ChannelId}", mcde.Data.ChannelId); - logger.LogInformation("Message Id: {MessageId}", mcde.Data.Id); - logger.LogInformation("User Id: {UserId}", mcde.Data.Author.Id); - return Task.CompletedTask; + return; } - return Task.CompletedTask; + 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);