feat: implement sending welcome message
This commit is contained in:
@@ -25,7 +25,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
private readonly AsyncLock _lock = new();
|
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 string _gatewayUrl = string.Empty;
|
||||||
private IWebSocket? _webSocket;
|
private IWebSocket? _webSocket;
|
||||||
@@ -85,7 +85,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
await CloseIfOpenAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", cancellationToken);
|
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)
|
if (DiscordEventTypes.IsValidEvent(eventName) is false)
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
_eventHandlers[eventName] += handler;
|
_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)
|
if (DiscordEventTypes.IsValidEvent(eventName) is false)
|
||||||
{
|
{
|
||||||
@@ -271,7 +271,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
{
|
{
|
||||||
await using var scope = _serviceScopeFactory.CreateAsyncScope();
|
await using var scope = _serviceScopeFactory.CreateAsyncScope();
|
||||||
// TODO: Pass cancellation token to handler
|
// 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
|
# pragma warning disable CA1031 // Do not catch general exception types
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ internal sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
|
|
||||||
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken)
|
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var uri = new Uri("gateway", UriKind.Relative);
|
var gatewayEndpoint = new Uri("gateway", UriKind.Relative);
|
||||||
var response = await _httpClient.GetAsync(uri, cancellationToken);
|
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);
|
_logger.LogError("Failed to get gateway URL: {StatusCode}", response.StatusCode);
|
||||||
throw new DiscordRestClientException("Failed to get gateway URL.");
|
throw new DiscordRestClientException("Failed to get gateway URL.");
|
||||||
@@ -36,8 +36,49 @@ internal sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
|
|
||||||
return gatewayResponse.Url;
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 record GatewayResponse(
|
internal sealed record GatewayResponse(
|
||||||
[property: JsonPropertyName("url")] string Url
|
[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 ConnectAsync(CancellationToken cancellationToken);
|
||||||
Task DisconnectAsync(CancellationToken cancellationToken);
|
Task DisconnectAsync(CancellationToken cancellationToken);
|
||||||
void On(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler);
|
void On(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> handler);
|
||||||
void Off(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler);
|
void Off(string eventName, Func<DiscordEvent, IServiceProvider, CancellationToken, Task> handler);
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ namespace StevesBot.Worker.Discord;
|
|||||||
internal interface IDiscordRestClient
|
internal interface IDiscordRestClient
|
||||||
{
|
{
|
||||||
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
|
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
|
||||||
|
Task<MessageCreateData> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
@@ -22,6 +24,7 @@ builder.Services
|
|||||||
var discordOptions = sp.GetRequiredService<DiscordClientOptions>();
|
var discordOptions = sp.GetRequiredService<DiscordClientOptions>();
|
||||||
c.BaseAddress = new Uri(discordOptions.ApiUrl);
|
c.BaseAddress = new Uri(discordOptions.ApiUrl);
|
||||||
c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken);
|
c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken);
|
||||||
|
c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)");
|
||||||
})
|
})
|
||||||
.AddStandardResilienceHandler();
|
.AddStandardResilienceHandler();
|
||||||
|
|
||||||
|
|||||||
@@ -16,34 +16,32 @@ internal class Worker : IHostedService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Connecting Discord Gateway Client");
|
_logger.LogInformation("Connecting Discord Gateway Client");
|
||||||
|
|
||||||
// TODO: Provide access to the gateway client
|
_discordGatewayClient.On(DiscordEventTypes.MessageCreate, static async (discordEvent, sp, cancellationToken) =>
|
||||||
// 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) =>
|
|
||||||
{
|
{
|
||||||
var discordRestClient = sp.GetRequiredService<IDiscordRestClient>();
|
var discordRestClient = sp.GetRequiredService<IDiscordRestClient>();
|
||||||
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
|
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
|
||||||
|
|
||||||
// TODO: When a user join message is received,
|
if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false)
|
||||||
// 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)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Guild Id: {GuildId}", mcde.Data.GuildId);
|
return;
|
||||||
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 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);
|
return _discordGatewayClient.ConnectAsync(cancellationToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user