feat: implement sending welcome message

This commit is contained in:
Stevan Freeborn
2025-05-17 15:54:00 -05:00
parent 6ba7595766
commit db221334ca
6 changed files with 74 additions and 31 deletions
@@ -25,7 +25,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
},
};
private readonly AsyncLock _lock = new();
private readonly Dictionary<string, Func<DiscordEvent, IServiceProvider, Task>> _eventHandlers = [];
private readonly Dictionary<string, Func<DiscordEvent, IServiceProvider, CancellationToken, Task>> _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<DiscordEvent, IServiceProvider, Task> handler)
public void On(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> 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<DiscordEvent, IServiceProvider, Task> handler)
public void Off(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> 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)
@@ -17,10 +17,10 @@ internal sealed class DiscordRestClient : IDiscordRestClient
public async Task<string> 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<MessageCreateData> 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.");
}
internal record GatewayResponse(
var messageCreateData = await response.Content.ReadFromJsonAsync<MessageCreateData>(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 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;
}
@@ -4,6 +4,6 @@ internal interface IDiscordGatewayClient : IDisposable
{
Task ConnectAsync(CancellationToken cancellationToken);
Task DisconnectAsync(CancellationToken cancellationToken);
void On(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler);
void Off(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler);
void On(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> handler);
void Off(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> handler);
}
@@ -3,4 +3,5 @@ namespace StevesBot.Worker.Discord;
internal interface IDiscordRestClient
{
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
Task<MessageCreateData> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken);
}
+3
View File
@@ -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<DiscordClientOptions>();
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();
+19 -21
View File
@@ -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<IDiscordRestClient>();
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
// 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);