refactor: organize things a bit
This commit is contained in:
@@ -1,37 +0,0 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
|
||||||
|
|
||||||
internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent
|
|
||||||
{
|
|
||||||
[JsonPropertyName("d")]
|
|
||||||
public new MessageCreateData Data { get; init; } = new MessageCreateData();
|
|
||||||
|
|
||||||
public bool IsMessageType(int messageType)
|
|
||||||
{
|
|
||||||
return Data.Type == messageType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed record MessageCreateData
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("type")]
|
|
||||||
public int Type { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("channel_id")]
|
|
||||||
public string ChannelId { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("guild_id")]
|
|
||||||
public string GuildId { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[JsonPropertyName("author")]
|
|
||||||
public DiscordUser Author { get; init; } = new DiscordUser();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed record DiscordUser
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public string Id { get; init; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
namespace StevesBot.Worker.Discord;
|
||||||
|
|
||||||
|
internal static class Extensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDiscordRestClient(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddHttpClient<IDiscordRestClient, DiscordRestClient>(static (sp, c) =>
|
||||||
|
{
|
||||||
|
var discordOptions = sp.GetRequiredService<DiscordClientOptions>();
|
||||||
|
c.BaseAddress = new Uri(discordOptions.ApiUrl);
|
||||||
|
c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken);
|
||||||
|
c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)");
|
||||||
|
})
|
||||||
|
.AddStandardResilienceHandler();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddDiscordGatewayClient(this IServiceCollection services, Action<IDiscordGatewayClient>? configure)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDiscordGatewayClient>(sp =>
|
||||||
|
{
|
||||||
|
var discordOptions = sp.GetRequiredService<DiscordClientOptions>();
|
||||||
|
var discordRestClient = sp.GetRequiredService<IDiscordRestClient>();
|
||||||
|
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
|
||||||
|
var socketFactory = sp.GetRequiredService<IWebSocketFactory>();
|
||||||
|
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
||||||
|
var serviceScopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
|
||||||
|
var client = new DiscordGatewayClient(
|
||||||
|
discordOptions,
|
||||||
|
socketFactory,
|
||||||
|
logger,
|
||||||
|
discordRestClient,
|
||||||
|
timeProvider,
|
||||||
|
serviceScopeFactory
|
||||||
|
);
|
||||||
|
|
||||||
|
if (configure is not null)
|
||||||
|
{
|
||||||
|
configure(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Gateway;
|
||||||
|
|
||||||
internal static class DiscordCloseCodes
|
internal static class DiscordCloseCodes
|
||||||
{
|
{
|
||||||
+12
-64
@@ -1,6 +1,4 @@
|
|||||||
using System.Text;
|
namespace StevesBot.Worker.Discord.Gateway;
|
||||||
|
|
||||||
namespace StevesBot.Worker.Discord;
|
|
||||||
|
|
||||||
internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
||||||
{
|
{
|
||||||
@@ -129,8 +127,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
|
|
||||||
private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken)
|
private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await CancelReceiveMessagesTaskAsync(cancellationToken);
|
|
||||||
|
|
||||||
var newReceiveCts = new CancellationTokenSource();
|
var newReceiveCts = new CancellationTokenSource();
|
||||||
var newReceiveLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newReceiveCts.Token);
|
var newReceiveLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newReceiveCts.Token);
|
||||||
|
|
||||||
@@ -164,10 +160,8 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
var canResume = IsResumableCloseCode(result.CloseStatus);
|
var canResume = IsResumableCloseCode(result.CloseStatus);
|
||||||
var closeStatus = canResume ? WebSocketCloseStatus.MandatoryExtension : WebSocketCloseStatus.NormalClosure;
|
|
||||||
|
|
||||||
await SetCanResumeAsync(canResume, _linkedReceiveMessageCts.Token);
|
await SetCanResumeAsync(canResume, _linkedReceiveMessageCts.Token);
|
||||||
await CloseIfOpenAsync(closeStatus, result.CloseStatusDescription, _linkedReceiveMessageCts.Token);
|
|
||||||
|
|
||||||
_logger.LogWarning("Reconnecting because of close message: {CloseStatus} - {CloseStatusDescription}", result.CloseStatus, result.CloseStatusDescription);
|
_logger.LogWarning("Reconnecting because of close message: {CloseStatus} - {CloseStatusDescription}", result.CloseStatus, result.CloseStatusDescription);
|
||||||
|
|
||||||
@@ -213,12 +207,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
|
|
||||||
_logger.LogWarning("Closing connection and invalidating session.");
|
_logger.LogWarning("Closing connection and invalidating session.");
|
||||||
|
|
||||||
await CloseIfOpenAsync(
|
|
||||||
WebSocketCloseStatus.NormalClosure,
|
|
||||||
"WebSocket error. Closing connection and invalidating session.",
|
|
||||||
newReceiveLinkedCts.Token
|
|
||||||
);
|
|
||||||
|
|
||||||
await SetCanResumeAsync(false, newReceiveLinkedCts.Token);
|
await SetCanResumeAsync(false, newReceiveLinkedCts.Token);
|
||||||
|
|
||||||
_logger.LogWarning("Reconnecting because of error in receive message task");
|
_logger.LogWarning("Reconnecting because of error in receive message task");
|
||||||
@@ -293,12 +281,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
|
|
||||||
await SetCanResumeAsync(true, cancellationToken);
|
await SetCanResumeAsync(true, cancellationToken);
|
||||||
|
|
||||||
await CloseIfOpenAsync(
|
|
||||||
WebSocketCloseStatus.MandatoryExtension,
|
|
||||||
"Reconnect event received",
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
_logger.LogWarning("Reconnecting because of reconnect event");
|
_logger.LogWarning("Reconnecting because of reconnect event");
|
||||||
|
|
||||||
await ReconnectAsync(cancellationToken);
|
await ReconnectAsync(cancellationToken);
|
||||||
@@ -311,24 +293,12 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
_logger.LogWarning("Session is resumable. Closing connection without invalidating session.");
|
_logger.LogWarning("Session is resumable. Closing connection without invalidating session.");
|
||||||
|
|
||||||
await SetCanResumeAsync(true, cancellationToken);
|
await SetCanResumeAsync(true, cancellationToken);
|
||||||
|
|
||||||
await CloseIfOpenAsync(
|
|
||||||
WebSocketCloseStatus.MandatoryExtension,
|
|
||||||
"Invalid session event received",
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Session is not resumable. Closing connection and invalidating session.");
|
_logger.LogWarning("Session is not resumable. Closing connection and invalidating session.");
|
||||||
|
|
||||||
await SetCanResumeAsync(false, cancellationToken);
|
await SetCanResumeAsync(false, cancellationToken);
|
||||||
|
|
||||||
await CloseIfOpenAsync(
|
|
||||||
WebSocketCloseStatus.NormalClosure,
|
|
||||||
"Invalid session event received",
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ReconnectAsync(cancellationToken);
|
await ReconnectAsync(cancellationToken);
|
||||||
@@ -341,8 +311,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
|
|
||||||
private async Task StartHeartbeatAsync(CancellationToken cancellationToken)
|
private async Task StartHeartbeatAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await CancelHeartbeatTaskAsync(cancellationToken);
|
|
||||||
|
|
||||||
var newHeartbeatCts = new CancellationTokenSource();
|
var newHeartbeatCts = new CancellationTokenSource();
|
||||||
var newLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newHeartbeatCts.Token);
|
var newLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, newHeartbeatCts.Token);
|
||||||
|
|
||||||
@@ -365,14 +333,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
{
|
{
|
||||||
if (IsWebSocketOpen())
|
if (IsWebSocketOpen())
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Heartbeat not acknowledged. Closing WebSocket.");
|
|
||||||
|
|
||||||
await CloseAsync(
|
|
||||||
WebSocketCloseStatus.ProtocolError,
|
|
||||||
"Heartbeat not acknowledged",
|
|
||||||
_linkedHeartbeatCts.Token
|
|
||||||
);
|
|
||||||
|
|
||||||
await SetCanResumeAsync(true, _linkedHeartbeatCts.Token);
|
await SetCanResumeAsync(true, _linkedHeartbeatCts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,14 +357,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error in heartbeat task: {Message}", ex.Message);
|
_logger.LogError(ex, "Error in heartbeat task: {Message}", ex.Message);
|
||||||
|
|
||||||
_logger.LogWarning("Closing connection and invalidating session.");
|
|
||||||
|
|
||||||
await CloseIfOpenAsync(
|
|
||||||
WebSocketCloseStatus.NormalClosure,
|
|
||||||
"Heartbeat error. Closing connection and invalidating session.",
|
|
||||||
newLinkedCts.Token
|
|
||||||
);
|
|
||||||
|
|
||||||
_logger.LogWarning("Reconnecting because of error in heartbeat task");
|
_logger.LogWarning("Reconnecting because of error in heartbeat task");
|
||||||
|
|
||||||
await ReconnectAsync(cancellationToken);
|
await ReconnectAsync(cancellationToken);
|
||||||
@@ -415,6 +367,17 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
|
|
||||||
private async Task ReconnectAsync(CancellationToken cancellationToken)
|
private async Task ReconnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
await CancelReceiveMessagesTaskAsync(cancellationToken);
|
||||||
|
await CancelHeartbeatTaskAsync(cancellationToken);
|
||||||
|
|
||||||
|
var closeStatus = _canResume ? WebSocketCloseStatus.MandatoryExtension : WebSocketCloseStatus.NormalClosure;
|
||||||
|
|
||||||
|
await CloseIfOpenAsync(
|
||||||
|
closeStatus,
|
||||||
|
"Reconnecting",
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
_webSocket?.Dispose();
|
_webSocket?.Dispose();
|
||||||
|
|
||||||
if (_canResume)
|
if (_canResume)
|
||||||
@@ -638,21 +601,6 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient
|
|||||||
await _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);
|
await _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_webSocket is null)
|
|
||||||
{
|
|
||||||
throw new DiscordGatewayClientException("WebSocket is not set. Cannot close.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_webSocket.State is not WebSocketState.Open)
|
|
||||||
{
|
|
||||||
throw new DiscordGatewayClientException("WebSocket is not open. Cannot close.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<WebSocketReceiveResult> ReceiveMessageAsync(
|
private async Task<WebSocketReceiveResult> ReceiveMessageAsync(
|
||||||
ArraySegment<byte> segment,
|
ArraySegment<byte> segment,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Gateway;
|
||||||
|
|
||||||
internal sealed class DiscordGatewayClientException : Exception
|
internal sealed class DiscordGatewayClientException : Exception
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Gateway;
|
||||||
|
|
||||||
internal static class DiscordIntents
|
internal static class DiscordIntents
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal record DiscordEvent
|
internal record DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed class DiscordEventConverter : JsonConverter<DiscordEvent>
|
internal sealed class DiscordEventConverter : JsonConverter<DiscordEvent>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal static class DiscordEventTypes
|
internal static class DiscordEventTypes
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal static class DiscordMessageTypes
|
internal static class DiscordMessageTypes
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal static class DiscordOpCodes
|
internal static class DiscordOpCodes
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal record DispatchDiscordEvent : DiscordEvent
|
internal record DispatchDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record HeartbeatAckDiscordEvent : DiscordEvent
|
internal sealed record HeartbeatAckDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record HeartbeatDiscordEvent : DiscordEvent
|
internal sealed record HeartbeatDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record HelloDiscordEvent : DiscordEvent
|
internal sealed record HelloDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record IdentifyDiscordEvent : DiscordEvent
|
internal sealed record IdentifyDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record InvalidSessionDiscordEvent : DiscordEvent
|
internal sealed record InvalidSessionDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
|
internal sealed record MessageCreateDiscordEvent : DispatchDiscordEvent
|
||||||
|
{
|
||||||
|
[JsonPropertyName("d")]
|
||||||
|
public new DiscordMessage Data { get; init; } = new DiscordMessage();
|
||||||
|
|
||||||
|
public bool IsMessageType(int messageType)
|
||||||
|
{
|
||||||
|
return Data.Type == messageType;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record ReadyDiscordEvent : DispatchDiscordEvent
|
internal sealed record ReadyDiscordEvent : DispatchDiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record ReconnectDiscordEvent : DiscordEvent
|
internal sealed record ReconnectDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record ResumeDiscordEvent : DiscordEvent
|
internal sealed record ResumeDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord.Events;
|
namespace StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
|
||||||
internal sealed record UpdatePresenceDiscordEvent : DiscordEvent
|
internal sealed record UpdatePresenceDiscordEvent : DiscordEvent
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Gateway;
|
||||||
|
|
||||||
internal interface IDiscordGatewayClient : IDisposable
|
internal interface IDiscordGatewayClient : IDisposable
|
||||||
{
|
{
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
|
||||||
|
|
||||||
internal interface IDiscordRestClient
|
|
||||||
{
|
|
||||||
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
|
|
||||||
Task<MessageCreateData> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
+5
-29
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Rest;
|
||||||
|
|
||||||
internal sealed class DiscordRestClient : IDiscordRestClient
|
internal sealed class DiscordRestClient : IDiscordRestClient
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,6 @@ internal sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken)
|
public async Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var gatewayEndpoint = new Uri("gateway", UriKind.Relative);
|
var gatewayEndpoint = new Uri("gateway", UriKind.Relative);
|
||||||
@@ -37,7 +36,7 @@ internal sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
return gatewayResponse.Url;
|
return gatewayResponse.Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MessageCreateData> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken)
|
public async Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative);
|
var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative);
|
||||||
var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken);
|
var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken);
|
||||||
@@ -48,37 +47,14 @@ internal sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
throw new DiscordRestClientException("Failed to create message.");
|
throw new DiscordRestClientException("Failed to create message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageCreateData = await response.Content.ReadFromJsonAsync<MessageCreateData>(cancellationToken);
|
var discordMessage = await response.Content.ReadFromJsonAsync<DiscordMessage>(cancellationToken);
|
||||||
|
|
||||||
if (messageCreateData is null)
|
if (discordMessage is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to deserialize message create response.");
|
_logger.LogError("Failed to deserialize message create response.");
|
||||||
throw new DiscordRestClientException("Failed to deserialize message create response.");
|
throw new DiscordRestClientException("Failed to deserialize message create response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageCreateData;
|
return discordMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record GatewayResponse(
|
|
||||||
[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;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Rest;
|
||||||
|
|
||||||
internal sealed class DiscordRestClientException : Exception
|
internal sealed class DiscordRestClientException : Exception
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Rest;
|
||||||
|
|
||||||
|
internal interface IDiscordRestClient
|
||||||
|
{
|
||||||
|
Task<string> GetGatewayUrlAsync(CancellationToken cancellationToken);
|
||||||
|
Task<DiscordMessage> CreateMessageAsync(string channelId, CreateMessageRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Rest.Requests;
|
||||||
|
|
||||||
|
internal sealed record CreateMessageRequest(
|
||||||
|
[property: JsonPropertyName("content")] string Content,
|
||||||
|
[property: JsonPropertyName("message_reference")] MessageReference? MessageReference
|
||||||
|
);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Rest.Responses;
|
||||||
|
|
||||||
|
internal sealed record GatewayResponse(
|
||||||
|
[property: JsonPropertyName("url")] string Url
|
||||||
|
);
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace StevesBot.Worker.Discord;
|
namespace StevesBot.Worker.Discord.Shared;
|
||||||
|
|
||||||
internal sealed class DiscordClientOptions
|
internal sealed class DiscordClientOptions
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Shared;
|
||||||
|
|
||||||
|
internal sealed record DiscordMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public int Type { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("channel_id")]
|
||||||
|
public string ChannelId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("guild_id")]
|
||||||
|
public string GuildId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("author")]
|
||||||
|
public DiscordUser Author { get; init; } = new DiscordUser();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Shared;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace StevesBot.Worker.Discord.Shared;
|
||||||
|
|
||||||
|
internal sealed record DiscordUser
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace StevesBot.Worker.Handlers;
|
||||||
|
|
||||||
|
internal static class WelcomeMessageHandler
|
||||||
|
{
|
||||||
|
public static async Task HandleAsync(
|
||||||
|
DiscordEvent discordEvent,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var discordRestClient = serviceProvider.GetRequiredService<IDiscordRestClient>();
|
||||||
|
var logger = serviceProvider.GetRequiredService<ILogger<DiscordGatewayClient>>();
|
||||||
|
|
||||||
|
if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Received user join message for user: {UserId}", mcde.Data.Author.Id);
|
||||||
|
var welcomeMessage = GetWelcomeMessage(mcde.Data.Author.Id);
|
||||||
|
|
||||||
|
var request = new CreateMessageRequest(
|
||||||
|
Content: welcomeMessage,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetWelcomeMessage(string userId)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
builder.AppendLine(CultureInfo.InvariantCulture, $"Hi there <@{userId}>! 👋🏻 Welcome to Stevan's server.");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("We're so stoked to have you join our community here. Feel free to jump right in, tell us a bit about yourself, and explore all the different channels.");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("If you have any questions at all, don't hesitate to ask. We're a friendly bunch and always happy to help out. Glad you're here!");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
using System.Net.Http.Headers;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
builder.Services.Configure<HostOptions>(static options => options.ShutdownTimeout = TimeSpan.FromSeconds(30));
|
builder.Services.Configure<HostOptions>(static options => options.ShutdownTimeout = TimeSpan.FromSeconds(30));
|
||||||
@@ -18,20 +14,11 @@ builder.Services.AddSingleton(static sp =>
|
|||||||
builder.Services.AddSingleton<IWebSocketFactory, WebSocketFactory>();
|
builder.Services.AddSingleton<IWebSocketFactory, WebSocketFactory>();
|
||||||
builder.Services.AddSingleton(TimeProvider.System);
|
builder.Services.AddSingleton(TimeProvider.System);
|
||||||
|
|
||||||
builder.Services
|
builder.Services.AddDiscordRestClient();
|
||||||
.AddHttpClient<IDiscordRestClient, DiscordRestClient>(static (sp, c) =>
|
|
||||||
{
|
|
||||||
var discordOptions = sp.GetRequiredService<DiscordClientOptions>();
|
|
||||||
c.BaseAddress = new Uri(discordOptions.ApiUrl);
|
|
||||||
c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken);
|
|
||||||
c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)");
|
|
||||||
})
|
|
||||||
.AddStandardResilienceHandler();
|
|
||||||
|
|
||||||
// TODO: Create an extension method that allows adding
|
builder.Services.AddDiscordGatewayClient(static (client) =>
|
||||||
// the discord gateway client and allows me to configure
|
client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync)
|
||||||
// event handlers
|
);
|
||||||
builder.Services.AddSingleton<IDiscordGatewayClient, DiscordGatewayClient>();
|
|
||||||
|
|
||||||
builder.Services.AddHostedService<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
global using System.Net.Http.Json;
|
global using System.Net.Http.Json;
|
||||||
global using System.Net.WebSockets;
|
global using System.Net.WebSockets;
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
|
global using System.Text;
|
||||||
global using System.Text.Json;
|
global using System.Text.Json;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
|
||||||
global using StevesBot.Worker.Discord.Events;
|
global using Microsoft.Extensions.Options;
|
||||||
global using StevesBot.Worker.Threading;
|
|
||||||
global using StevesBot.Worker.WebSockets;
|
|
||||||
|
|
||||||
global using StevesBot.Worker;
|
global using StevesBot.Worker;
|
||||||
global using StevesBot.Worker.Discord;
|
global using StevesBot.Worker.Discord;
|
||||||
|
global using StevesBot.Worker.Discord.Gateway;
|
||||||
|
global using StevesBot.Worker.Discord.Gateway.Events;
|
||||||
|
global using StevesBot.Worker.Discord.Rest;
|
||||||
|
global using StevesBot.Worker.Discord.Rest.Requests;
|
||||||
|
global using StevesBot.Worker.Discord.Rest.Responses;
|
||||||
|
global using StevesBot.Worker.Discord.Shared;
|
||||||
|
global using StevesBot.Worker.Handlers;
|
||||||
|
global using StevesBot.Worker.Threading;
|
||||||
|
global using StevesBot.Worker.WebSockets;
|
||||||
@@ -15,35 +15,6 @@ 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");
|
||||||
|
|
||||||
_discordGatewayClient.On(DiscordEventTypes.MessageCreate, static async (discordEvent, sp, cancellationToken) =>
|
|
||||||
{
|
|
||||||
var discordRestClient = sp.GetRequiredService<IDiscordRestClient>();
|
|
||||||
var logger = sp.GetRequiredService<ILogger<DiscordGatewayClient>>();
|
|
||||||
|
|
||||||
if (discordEvent is not MessageCreateDiscordEvent mcde || mcde.IsMessageType(DiscordMessageTypes.UserJoin) == false)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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