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 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);
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user