feat: expose methods on client to register event delegates
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user