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 IDiscordRestClient _discordRestClient;
private readonly TimeProvider _timeProvider;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -24,6 +25,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
},
};
private readonly AsyncLock _lock = new();
private readonly Dictionary<string, Func<DiscordEvent, IServiceProvider, Task>> _eventHandlers = [];
private string _gatewayUrl = string.Empty;
private IWebSocket? _webSocket;
@@ -45,7 +47,8 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
IWebSocketFactory webSocketFactory,
ILogger<DiscordGatewayClient> logger,
IDiscordRestClient discordRestClient,
TimeProvider timeProvider
TimeProvider timeProvider,
IServiceScopeFactory serviceScopeFactory
)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -53,6 +56,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_discordRestClient = discordRestClient ?? throw new ArgumentNullException(nameof(discordRestClient));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
}
public async Task ConnectAsync(CancellationToken cancellationToken)
@@ -75,11 +79,54 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
await SendIdleStatusAsync(cancellationToken);
await CancelReceiveMessagesTaskAsync(cancellationToken);
await CancelHeartbeatTaskAsync(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)
{
await CancelReceiveMessagesTaskAsync(cancellationToken);
@@ -204,6 +251,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
break;
case DispatchDiscordEvent de:
await SetDispatchSequenceAsync(e.Sequence, cancellationToken);
var eventType = de.Type ?? "Unknown";
switch (de)
{
@@ -213,9 +261,30 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
_logger.LogInformation("Ready event received");
break;
default:
_logger.LogInformation("Received dispatch event: {Event}", de.Type ?? "Unknown");
_logger.LogInformation("Received dispatch event: {Event}", eventType);
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;
case ReconnectDiscordEvent:
_logger.LogInformation("Reconnect event received");
@@ -406,6 +475,23 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
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)
{
var heartbeat = new HeartbeatDiscordEvent(_lastSequence);
@@ -3,4 +3,14 @@ namespace StevesBot.Worker.Discord.Events;
internal static class DiscordEventTypes
{
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
{
public const string Online = "online";
public const string Idle = "idle";
}
internal sealed record Activity
@@ -4,4 +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);
}
+4
View File
@@ -25,7 +25,11 @@ builder.Services
})
.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.AddHostedService<Worker>();
var host = builder.Build();
+19
View File
@@ -15,6 +15,25 @@ internal class Worker : IHostedService
public Task StartAsync(CancellationToken cancellationToken)
{
_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);
}