feat: expose methods on client to register event delegates

This commit is contained in:
Stevan Freeborn
2025-05-15 22:52:51 -05:00
parent d6060f658e
commit 2bc63e0305
6 changed files with 124 additions and 2 deletions
@@ -14,6 +14,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
private readonly ILogger<DiscordGatewayClient> _logger; private readonly ILogger<DiscordGatewayClient> _logger;
private readonly IDiscordRestClient _discordRestClient; private readonly IDiscordRestClient _discordRestClient;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly JsonSerializerOptions _jsonSerializerOptions = new() private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -24,6 +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 string _gatewayUrl = string.Empty; private string _gatewayUrl = string.Empty;
private IWebSocket? _webSocket; private IWebSocket? _webSocket;
@@ -45,7 +47,8 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
IWebSocketFactory webSocketFactory, IWebSocketFactory webSocketFactory,
ILogger<DiscordGatewayClient> logger, ILogger<DiscordGatewayClient> logger,
IDiscordRestClient discordRestClient, IDiscordRestClient discordRestClient,
TimeProvider timeProvider TimeProvider timeProvider,
IServiceScopeFactory serviceScopeFactory
) )
{ {
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
@@ -53,6 +56,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_discordRestClient = discordRestClient ?? throw new ArgumentNullException(nameof(discordRestClient)); _discordRestClient = discordRestClient ?? throw new ArgumentNullException(nameof(discordRestClient));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
} }
public async Task ConnectAsync(CancellationToken cancellationToken) public async Task ConnectAsync(CancellationToken cancellationToken)
@@ -75,11 +79,54 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
public async Task DisconnectAsync(CancellationToken cancellationToken) public async Task DisconnectAsync(CancellationToken cancellationToken)
{ {
await SendIdleStatusAsync(cancellationToken);
await CancelReceiveMessagesTaskAsync(cancellationToken); await CancelReceiveMessagesTaskAsync(cancellationToken);
await CancelHeartbeatTaskAsync(cancellationToken); await CancelHeartbeatTaskAsync(cancellationToken);
await CloseIfOpenAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", cancellationToken); await CloseIfOpenAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", cancellationToken);
} }
public void On(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler)
{
if (DiscordEventTypes.IsValidEvent(eventName) is false)
{
throw new ArgumentException($"Invalid event name: {eventName}", nameof(eventName));
}
ArgumentNullException.ThrowIfNull(handler);
if (_eventHandlers.ContainsKey(eventName) is false)
{
_eventHandlers[eventName] = handler;
return;
}
_eventHandlers[eventName] += handler;
}
public void Off(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler)
{
if (DiscordEventTypes.IsValidEvent(eventName) is false)
{
throw new ArgumentException($"Invalid event name: {eventName}", nameof(eventName));
}
ArgumentNullException.ThrowIfNull(handler);
if (_eventHandlers.TryGetValue(eventName, out var handlers))
{
handlers -= handler;
if (handlers is null)
{
_eventHandlers.Remove(eventName);
}
else
{
_eventHandlers[eventName] = handlers;
}
}
}
private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken) private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken)
{ {
await CancelReceiveMessagesTaskAsync(cancellationToken); await CancelReceiveMessagesTaskAsync(cancellationToken);
@@ -204,6 +251,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
break; break;
case DispatchDiscordEvent de: case DispatchDiscordEvent de:
await SetDispatchSequenceAsync(e.Sequence, cancellationToken); await SetDispatchSequenceAsync(e.Sequence, cancellationToken);
var eventType = de.Type ?? "Unknown";
switch (de) switch (de)
{ {
@@ -213,9 +261,30 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
_logger.LogInformation("Ready event received"); _logger.LogInformation("Ready event received");
break; break;
default: default:
_logger.LogInformation("Received dispatch event: {Event}", de.Type ?? "Unknown"); _logger.LogInformation("Received dispatch event: {Event}", eventType);
break; break;
} }
if (_eventHandlers.TryGetValue(eventType, out var handler))
{
try
{
await using var scope = _serviceScopeFactory.CreateAsyncScope();
// TODO: Pass cancellation token to handler
await handler(de, scope.ServiceProvider);
}
# pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
{
_logger.LogError(ex, "Error handling event: {Event}", eventType);
}
# pragma warning restore CA1031 // Do not catch general exception types
}
else
{
_logger.LogInformation("No handler for event: {Event}", eventType);
}
break; break;
case ReconnectDiscordEvent: case ReconnectDiscordEvent:
_logger.LogInformation("Reconnect event received"); _logger.LogInformation("Reconnect event received");
@@ -406,6 +475,23 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
await SendJsonAsync(identify, cancellationToken); await SendJsonAsync(identify, cancellationToken);
} }
private async Task SendIdleStatusAsync(CancellationToken cancellationToken)
{
var presence = new UpdatePresenceDiscordEvent(
_timeProvider.GetUtcNow().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
);
await SendJsonAsync(presence, cancellationToken);
}
private async Task SendHeartbeatAsync(CancellationToken cancellationToken) private async Task SendHeartbeatAsync(CancellationToken cancellationToken)
{ {
var heartbeat = new HeartbeatDiscordEvent(_lastSequence); var heartbeat = new HeartbeatDiscordEvent(_lastSequence);
@@ -3,4 +3,14 @@ namespace StevesBot.Worker.Discord.Events;
internal static class DiscordEventTypes internal static class DiscordEventTypes
{ {
public const string Ready = "READY"; public const string Ready = "READY";
public static bool IsValidEvent(string eventName)
{
if (string.IsNullOrEmpty(eventName))
{
return false;
}
return eventName is Ready;
}
} }
@@ -36,6 +36,7 @@ internal sealed record UpdatePresenceData
internal static class PresenceStatus internal static class PresenceStatus
{ {
public const string Online = "online"; public const string Online = "online";
public const string Idle = "idle";
} }
internal sealed record Activity internal sealed record Activity
@@ -4,4 +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 Off(string eventName, Func<DiscordEvent, IServiceProvider, Task> handler);
} }
+4
View File
@@ -25,7 +25,11 @@ builder.Services
}) })
.AddStandardResilienceHandler(); .AddStandardResilienceHandler();
// TODO: Create an extension method that allows adding
// the discord gateway client and allows me to configure
// event handlers
builder.Services.AddSingleton<IDiscordGatewayClient, DiscordGatewayClient>(); builder.Services.AddSingleton<IDiscordGatewayClient, DiscordGatewayClient>();
builder.Services.AddHostedService<Worker>(); builder.Services.AddHostedService<Worker>();
var host = builder.Build(); var host = builder.Build();
+19
View File
@@ -15,6 +15,25 @@ internal class Worker : IHostedService
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Connecting Discord Gateway Client"); _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.Ready, static (discordEvent, sp) =>
{
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
logger.LogInformation("Ready handler 1");
return Task.CompletedTask;
});
_discordGatewayClient.On(DiscordEventTypes.Ready, static (discordEvent, sp) =>
{
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
logger.LogInformation("Ready handler 2");
return Task.CompletedTask;
});
return _discordGatewayClient.ConnectAsync(cancellationToken); return _discordGatewayClient.ConnectAsync(cancellationToken);
} }