From 0c470bbcee2f06eb0dab2287a92b7a95d11fc164 Mon Sep 17 00:00:00 2001 From: Gehongyan Date: Tue, 7 May 2024 23:22:43 +0800 Subject: [PATCH] More socket message tests --- .../Messages/Cards/Modules/FileModule.cs | 8 +- src/Kook.Net.Core/Utils/UrlValidation.cs | 2 +- .../API/Common/Cards/Modules/FileModule.cs | 6 + .../Entities/Messages/MessageHelper.cs | 2 +- .../Entities/Messages/RestUserMessage.cs | 4 + .../Extensions/EntityExtensions.cs | 2 +- .../Entities/Messages/SocketUserMessage.cs | 12 + .../Extensions/TaskExtension.cs | 62 +++++ .../Fixtures/KookSocketClientFixture.cs | 13 +- .../Fixtures/SocketChannelFixture.cs | 12 +- .../Fixtures/SocketGuildFixture.cs | 6 +- .../MessageTests.cs | 246 +++++++++++++++++- 12 files changed, 348 insertions(+), 27 deletions(-) create mode 100644 test/Kook.Net.Tests.Integration.Socket/Extensions/TaskExtension.cs diff --git a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs index 7709c7e4..8e948edb 100644 --- a/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs +++ b/src/Kook.Net.Core/Entities/Messages/Cards/Modules/FileModule.cs @@ -9,10 +9,11 @@ namespace Kook; [DebuggerDisplay("{DebuggerDisplay,nq}")] public class FileModule : IMediaModule, IEquatable, IEquatable { - internal FileModule(string source, string? title) + internal FileModule(string source, string? title, int? size = null) { Source = source; Title = title; + Size = size; } /// @@ -24,6 +25,11 @@ internal FileModule(string source, string? title) /// public string? Title { get; } + /// + /// The size of the file in bytes. + /// + public int? Size { get; } + private string DebuggerDisplay => $"{Type}: {Title}"; /// diff --git a/src/Kook.Net.Core/Utils/UrlValidation.cs b/src/Kook.Net.Core/Utils/UrlValidation.cs index cfa585ba..dbcff26c 100644 --- a/src/Kook.Net.Core/Utils/UrlValidation.cs +++ b/src/Kook.Net.Core/Utils/UrlValidation.cs @@ -53,7 +53,7 @@ public static bool ValidateKookAssetUrl(string url) if (string.IsNullOrEmpty(url)) return false; if (!Regex.IsMatch(url, - @"^https?:\/\/(img\.(kaiheila|kookapp)\.cn|kaiheila\.oss-cn-beijing\.aliyuncs\.com)\/(assets|attachments)\/\d{4}-\d{2}(\/\d{2})?\/\w{8,16}\.\w+$", + @"^https?:\/\/(img\.(kaiheila|kookapp)\.cn|kaiheila\.oss-cn-beijing\.aliyuncs\.com)\/(assets|attachments)\/\d{4}-\d{2}(\/\d{2})?\/\w{8,16}(\.\w+)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase)) throw new InvalidOperationException($"The url {url} must be a valid Kook asset URL"); diff --git a/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs b/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs index 9a92ca68..2514db5a 100644 --- a/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs +++ b/src/Kook.Net.Rest/API/Common/Cards/Modules/FileModule.cs @@ -9,4 +9,10 @@ internal class FileModule : ModuleBase [JsonPropertyName("title")] public string? Title { get; set; } + + [JsonPropertyName("size")] + public int? Size { get; set; } + + [JsonPropertyName("canDownload")] + public bool? CanDownload { get; set; } } diff --git a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs index f7af639c..16e4cd0c 100644 --- a/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Kook.Net.Rest/Entities/Messages/MessageHelper.cs @@ -297,7 +297,7 @@ public static IReadOnlyCollection ParseAttachments(IEnumerable switch (module) { case FileModule fileModule: - Attachment file = new(AttachmentType.File, fileModule.Source, fileModule.Title); + Attachment file = new(AttachmentType.File, fileModule.Source, fileModule.Title, fileModule.Size); attachments.Add(file); break; case AudioModule audioModule: diff --git a/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs index 91da9554..b0806fba 100644 --- a/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Kook.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -117,6 +117,8 @@ internal override void Update(Message model) else if (quotedMessageId.HasValue) Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, refMsgAuthor); } + else + Quote = null; if (model.Attachment is { } attachment) _attachments = [.._attachments, Attachment.Create(attachment)]; @@ -152,6 +154,8 @@ internal override void Update(DirectMessage model) else if (quotedMessageId.HasValue) Quote = global::Kook.Quote.Create(quotedMessageId.Value, quote.Type, quote.Content, quote.CreateAt, refMsgAuthor); } + else + Quote = null; if (model.Attachment is { } attachment) _attachments = [.._attachments, Attachment.Create(attachment)]; diff --git a/src/Kook.Net.Rest/Extensions/EntityExtensions.cs b/src/Kook.Net.Rest/Extensions/EntityExtensions.cs index 2a548784..6ff66bfa 100644 --- a/src/Kook.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Kook.Net.Rest/Extensions/EntityExtensions.cs @@ -141,7 +141,7 @@ public static DividerModule ToEntity(this API.DividerModule model) => new(); public static FileModule ToEntity(this API.FileModule model) => - new(model.Source, model.Title); + new(model.Source, model.Title, model.Size); public static AudioModule ToEntity(this API.AudioModule model) => new(model.Source, model.Title, model.Cover); diff --git a/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index dc9a92ba..ff6a99fb 100644 --- a/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Kook.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -145,6 +145,8 @@ internal override void Update(ClientState state, GatewayEvent +/// Adds extension methods to the class. +/// +public static class TaskExtension +{ + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15); + + /// + /// Waits for the task to complete within the specified timeout. + /// + /// The task to wait for. + /// The task. + /// The task did not complete within the specified timeout. + public static async Task WithTimeout(this Task task) => await task.WithTimeout(DefaultTimeout); + + /// + /// Waits for the task to complete within the specified timeout. + /// + /// The task to wait for. + /// A representing the timeout duration. + /// The task. + /// The task did not complete within the specified timeout. + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + Task delayTask = Task.Delay(timeout); + Task completedTask = await Task.WhenAny(task, delayTask); + if (completedTask == delayTask) + throw new TimeoutException(); + await task; + } + + /// + /// Waits for the task to complete within the specified timeout. + /// + /// The task to wait for. + /// The type of the task result. + /// The result of the task. + /// The task did not complete within the specified timeout. + public static Task WithTimeout(this Task task) => WithTimeout(task, DefaultTimeout); + + /// + /// Waits for the task to complete within the specified timeout. + /// + /// The task to wait for. + /// A representing the timeout duration. + /// The type of the task result. + /// The result of the task. + /// The task did not complete within the specified timeout. + public static async Task WithTimeout(this Task task, TimeSpan timeout) + { + Task delayTask = Task.Delay(timeout); + Task completedTask = await Task.WhenAny(task, delayTask); + if (completedTask == delayTask) + throw new TimeoutException(); + return await task; + } +} diff --git a/test/Kook.Net.Tests.Integration.Socket/Fixtures/KookSocketClientFixture.cs b/test/Kook.Net.Tests.Integration.Socket/Fixtures/KookSocketClientFixture.cs index 99f80cfd..816422fd 100644 --- a/test/Kook.Net.Tests.Integration.Socket/Fixtures/KookSocketClientFixture.cs +++ b/test/Kook.Net.Tests.Integration.Socket/Fixtures/KookSocketClientFixture.cs @@ -7,7 +7,7 @@ namespace Kook; public class KookSocketClientFixture : IDisposable, IAsyncDisposable { - private readonly TaskCompletionSource _ready = new(); + private readonly TaskCompletionSource _readyPromise = new(); public KookSocketClient Client { get; private set; } @@ -27,20 +27,21 @@ private async Task InitializeAsync() { LogLevel = LogSeverity.Debug, DefaultRetryMode = RetryMode.AlwaysRetry, - AlwaysDownloadUsers = false, - AlwaysDownloadBoostSubscriptions = false, - AlwaysDownloadVoiceStates = false + AlwaysDownloadUsers = true, + AlwaysDownloadBoostSubscriptions = true, + AlwaysDownloadVoiceStates = true, + MessageCacheSize = 100 }); Client.Ready += ClientOnReady; await Client.LoginAsync(TokenType.Bot, token); await Client.StartAsync(); - await _ready.Task; + await _readyPromise.Task.WithTimeout(); Client.Ready -= ClientOnReady; } private Task ClientOnReady() { - _ready.SetResult(); + _readyPromise.SetResult(); return Task.CompletedTask; } diff --git a/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketChannelFixture.cs b/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketChannelFixture.cs index 18dab661..b0898ae9 100644 --- a/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketChannelFixture.cs +++ b/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketChannelFixture.cs @@ -9,8 +9,8 @@ public class SocketChannelFixture : SocketGuildFixture private const string TextChannelName = "TEST TEXT CHANNEL"; private const string VoiceChannelName = "TEST VOICE CHANNEL"; - private readonly TaskCompletionSource _textChannelCreated = new(); - private readonly TaskCompletionSource _voiceChannelCreated = new(); + private readonly TaskCompletionSource _textChannelPromise = new(); + private readonly TaskCompletionSource _voiceChannelPromise = new(); public SocketTextChannel TextChannel { get; private set; } @@ -27,17 +27,17 @@ private async Task InitializeAsync() Client.ChannelCreated += OnChannelCreated; await Guild.CreateTextChannelAsync(TextChannelName); await Guild.CreateVoiceChannelAsync(VoiceChannelName); - TextChannel = await _textChannelCreated.Task; - VoiceChannel = await _voiceChannelCreated.Task; + TextChannel = await _textChannelPromise.Task.WithTimeout(); + VoiceChannel = await _voiceChannelPromise.Task.WithTimeout(); Client.ChannelCreated -= OnChannelCreated; } private Task OnChannelCreated(SocketChannel arg) { if (arg is SocketVoiceChannel { Name: VoiceChannelName } voiceChannel) - _voiceChannelCreated.SetResult(voiceChannel); + _voiceChannelPromise.SetResult(voiceChannel); else if (arg is SocketTextChannel { Name: TextChannelName } textChannel) - _textChannelCreated.SetResult(textChannel); + _textChannelPromise.SetResult(textChannel); return Task.CompletedTask; } diff --git a/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketGuildFixture.cs b/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketGuildFixture.cs index c35888df..c4726723 100644 --- a/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketGuildFixture.cs +++ b/test/Kook.Net.Tests.Integration.Socket/Fixtures/SocketGuildFixture.cs @@ -9,7 +9,7 @@ namespace Kook; public class SocketGuildFixture : KookSocketClientFixture { - private readonly TaskCompletionSource _joined = new(); + private readonly TaskCompletionSource _joinedPromise = new(); public SocketGuild Guild { get; private set; } @@ -41,7 +41,7 @@ private async Task InitializeAsync() { Client.JoinedGuild += OnJoinedGuild; await Client.CreateGuildAsync(guildName); - Guild = await _joined.Task; + Guild = await _joinedPromise.Task.WithTimeout(); Client.JoinedGuild -= OnJoinedGuild; } @@ -51,7 +51,7 @@ private async Task InitializeAsync() private Task OnJoinedGuild(SocketGuild guild) { - _joined.SetResult(guild); + _joinedPromise.SetResult(guild); return Task.CompletedTask; } diff --git a/test/Kook.Net.Tests.Integration.Socket/MessageTests.cs b/test/Kook.Net.Tests.Integration.Socket/MessageTests.cs index daaf9a20..3535279b 100644 --- a/test/Kook.Net.Tests.Integration.Socket/MessageTests.cs +++ b/test/Kook.Net.Tests.Integration.Socket/MessageTests.cs @@ -1,5 +1,10 @@ using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; using System.Threading.Tasks; +using Kook.Rest; using Kook.WebSocket; using Xunit; using Xunit.Abstractions; @@ -10,12 +15,14 @@ namespace Kook; [Trait("Category", "Integration.Socket")] public class MessageTests : IClassFixture, IAsyncDisposable { + private readonly HttpClient _httpClient; private readonly ITestOutputHelper _output; private readonly KookSocketClient _client; private readonly SocketTextChannel _textChannel; public MessageTests(SocketChannelFixture channelFixture, ITestOutputHelper output) { + _httpClient = new HttpClient(); _output = output; _textChannel = channelFixture.TextChannel; _client = channelFixture.Client; @@ -31,26 +38,134 @@ private Task LogAsync(LogMessage message) [Fact] public async Task CacheableShouldWork() { + // Send a message const string content = "CACHEABLE SHOULD WORK"; - Cacheable message = await _textChannel.SendTextAsync(content); - Assert.False(message.HasValue); - Assert.Null(message.Value); - IUserMessage? downloaded = await message.GetOrDownloadAsync(); + TaskCompletionSource socketMessagePromise = new(); + _client.MessageReceived += ClientOnMessageReceived; + Cacheable cacheableMessage = await _textChannel.SendTextAsync(content); + + // The cacheable should have no value + Assert.False(cacheableMessage.HasValue); + Assert.Null(cacheableMessage.Value); + + // The message should be able to be downloaded + IUserMessage? downloaded = await cacheableMessage.GetOrDownloadAsync(); Assert.NotNull(downloaded); Assert.Equal(content, downloaded.Content); + SocketTextChannel channel = Assert.IsType(downloaded.Channel); + + // The message should be able to be cached + await socketMessagePromise.Task.WithTimeout(); + SocketMessage? cachedMessage = channel.GetCachedMessage(downloaded.Id); + Assert.NotNull(cachedMessage); + Assert.Equal(downloaded.Id, cachedMessage.Id); + Assert.Equal(downloaded.Content, cachedMessage.Content); + + // Clean up + _client.MessageReceived -= ClientOnMessageReceived; + return; + + Task ClientOnMessageReceived(SocketMessage message, SocketGuildUser author, SocketTextChannel channel) + { + socketMessagePromise.SetResult(); + return Task.CompletedTask; + } } [Fact] public async Task SendTextAsync() { + // Send a text message const string content = "TEXT CONTENT"; TaskCompletionSource socketMessagePromise = new(); _client.MessageReceived += ClientOnMessageReceived; - Cacheable message = await _textChannel.SendTextAsync(content); - Guid messageId = message.Id; - SocketMessage socketMessage = await socketMessagePromise.Task; + Cacheable cacheableMessage = await _textChannel.SendTextAsync(content); + + // The message content received should be the same as the message sent + Guid messageId = cacheableMessage.Id; + SocketMessage socketMessage = await socketMessagePromise.Task.WithTimeout(); + Assert.Equal(MessageType.KMarkdown, socketMessage.Type); Assert.Equal(messageId, socketMessage.Id); Assert.Equal(content, socketMessage.Content); + + // Clean up + _client.MessageReceived -= ClientOnMessageReceived; + return; + + Task ClientOnMessageReceived(SocketMessage message, SocketGuildUser author, SocketTextChannel channel) + { + socketMessagePromise.SetResult(message); + return Task.CompletedTask; + } + } + + [Fact] + public async Task SendImageAsync() + { + // Send an image message + const string rawUri = "https://img.kaiheila.cn/assets/2021-01/7kr4FkWpLV0ku0ku.jpeg"; + const string filename = "7kr4FkWpLV0ku0ku.jpeg"; + TaskCompletionSource socketMessagePromise = new(); + _client.MessageReceived += ClientOnMessageReceived; + await using Stream imageStream = await _httpClient.GetStreamAsync(rawUri); + string assetUri = await _client.Rest.CreateAssetAsync(imageStream, filename); + FileAttachment fileAttachment = new(new Uri(assetUri), filename, AttachmentType.Image); + Cacheable cacheableMessage = await _textChannel.SendFileAsync(fileAttachment); + + // The message content received should be the same as the message sent + Guid messageId = cacheableMessage.Id; + SocketMessage socketMessage = await socketMessagePromise.Task.WithTimeout(); + Assert.Equal(MessageType.Image, socketMessage.Type); + Assert.Equal(messageId, socketMessage.Id); + Assert.Equal(assetUri, socketMessage.Content); + + // The message should have an attachment + Assert.Single(socketMessage.Attachments); + Attachment attachment = socketMessage.Attachments.Single(); + Assert.Equal(AttachmentType.Image, attachment.Type); + Assert.Equal(assetUri, attachment.Url); + Assert.Equal(filename, attachment.Filename); + + // Clean up + _client.MessageReceived -= ClientOnMessageReceived; + return; + + Task ClientOnMessageReceived(SocketMessage message, SocketGuildUser author, SocketTextChannel channel) + { + socketMessagePromise.SetResult(message); + return Task.CompletedTask; + } + } + + [Fact] + public async Task SendFileAsync() + { + // Send a file message + const string filename = "test.file"; + int fileSize = (int)(10 * Math.Pow(2, 20)); + TaskCompletionSource socketMessagePromise = new(); + _client.MessageReceived += ClientOnMessageReceived; + byte[] buffer = RandomNumberGenerator.GetBytes(fileSize); + using MemoryStream stream = new(buffer); + string assetUri = await _client.Rest.CreateAssetAsync(stream, filename); + FileAttachment fileAttachment = new(new Uri(assetUri), filename); + Cacheable cacheableMessage = await _textChannel.SendFileAsync(fileAttachment); + + // The message content received should be the same as the message sent + Guid messageId = cacheableMessage.Id; + SocketMessage socketMessage = await socketMessagePromise.Task.WithTimeout(); + Assert.Equal(MessageType.Card, socketMessage.Type); // File messages are converted to cards + Assert.Equal(messageId, socketMessage.Id); + + // The message should have an attachment + Assert.Single(socketMessage.Attachments); + Attachment attachment = socketMessage.Attachments.Single(); + Assert.Equal(AttachmentType.File, attachment.Type); + Assert.Equal(assetUri, attachment.Url); + Assert.Equal(filename, attachment.Filename); + Assert.Equal(fileSize, attachment.Size); + + // Clean up _client.MessageReceived -= ClientOnMessageReceived; return; @@ -61,9 +176,124 @@ Task ClientOnMessageReceived(SocketMessage message, SocketGuildUser author, Sock } } + [Fact] + public async Task MessageReferenceShouldWork() + { + int messageCount = 0; + TaskCompletionSource firstMessagePromise = new(); + TaskCompletionSource secondMessagePromise = new(); + _client.MessageReceived += ClientOnMessageReceived; + + // Send the first message + const string contentFirst = "MESSAGE 1"; + Cacheable cacheableFirst = await _textChannel.SendTextAsync(contentFirst); + SocketUserMessage firstMessage = await firstMessagePromise.Task.WithTimeout(); + + // Send the second message with referencing the first message + const string contentSecond = "MESSAGE 2"; + MessageReference reference = new(cacheableFirst.Id); + Cacheable cacheableSecond = await _textChannel.SendTextAsync(contentSecond, reference); + SocketUserMessage secondMessage = await secondMessagePromise.Task.WithTimeout(); + + // The second message should have a quote to the first message + Assert.Equal(cacheableFirst.Id, firstMessage.Id); + Assert.Equal(contentFirst, firstMessage.Content); + Assert.Equal(cacheableSecond.Id, secondMessage.Id); + Assert.Equal(contentSecond, secondMessage.Content); + Assert.NotNull(secondMessage.Quote); + Assert.Equal(firstMessage.Id, secondMessage.Quote.QuotedMessageId); + + // Modify the second message to remove the quote + TaskCompletionSource modificationPromise = new(); + _client.MessageUpdated += ClientOnMessageUpdated; + await secondMessage.ModifyAsync(x => + { + x.Quote = MessageReference.Empty; + }); + IUserMessage afterModification = await modificationPromise.Task.WithTimeout(); + Assert.Equal(secondMessage.Id, afterModification.Id); + Assert.Equal(contentSecond, afterModification.Content); + Assert.Null(afterModification.Quote); + + // Clean up + _client.MessageReceived -= ClientOnMessageReceived; + return; + + Task ClientOnMessageReceived(SocketMessage message, SocketGuildUser author, SocketTextChannel channel) + { + SocketUserMessage userMessage = Assert.IsType(message); + if (messageCount == 0) + firstMessagePromise.SetResult(userMessage); + else if (messageCount == 1) + secondMessagePromise.SetResult(userMessage); + messageCount++; + return Task.CompletedTask; + } + + async Task ClientOnMessageUpdated(Cacheable cacheableBefore, + Cacheable cacheableAfter, SocketTextChannel socketTextChannel) + { + IMessage? messageValueAfter = await cacheableAfter.GetOrDownloadAsync(); + Assert.NotNull(messageValueAfter); + IUserMessage userMessageAfter = Assert.IsType(messageValueAfter); + modificationPromise.SetResult(userMessageAfter); + } + } + + [Fact] + public async Task ModifyMessageAsync() + { + // Send a text message + const string content = "TEXT CONTENT"; + const string modifiedContent = "TEXT CONTENT MODIFIED"; + TaskCompletionSource beforePromise = new(); + TaskCompletionSource afterPromise = new(); + TaskCompletionSource channelPromise = new(); + Cacheable cacheableMessage = await _textChannel.SendTextAsync(content); + Guid messageId = cacheableMessage.Id; + + // Modify the message + _client.MessageUpdated += ClientOnMessageUpdated; + IUserMessage? message = await cacheableMessage.GetOrDownloadAsync(); + Assert.NotNull(message); + await message.ModifyAsync(x => + { + x.Content = modifiedContent; + }); + + // The message content modification should be received + IMessage messageBefore = await beforePromise.Task.WithTimeout(); + IMessage messageAfter = await afterPromise.Task.WithTimeout(); + SocketTextChannel channel = await channelPromise.Task.WithTimeout(); + Assert.Equal(messageId, messageBefore.Id); + Assert.Equal(content, messageBefore.Content); + Assert.Equal(messageId, messageAfter.Id); + Assert.Equal(modifiedContent, messageAfter.Content); + Assert.Equal(messageBefore.Channel.Id, messageAfter.Channel.Id); + Assert.NotSame(messageBefore, messageAfter); + Assert.Same(_textChannel, channel); + + // Clean up + _client.MessageUpdated -= ClientOnMessageUpdated; + return; + + async Task ClientOnMessageUpdated(Cacheable cacheableBefore, + Cacheable cacheableAfter, SocketTextChannel socketTextChannel) + { + IMessage? messageValueBefore = await cacheableBefore.GetOrDownloadAsync(); + Assert.NotNull(messageValueBefore); + beforePromise.SetResult(messageValueBefore); + IMessage? messageValueAfter = await cacheableAfter.GetOrDownloadAsync(); + Assert.NotNull(messageValueAfter); + afterPromise.SetResult(messageValueAfter); + channelPromise.SetResult(socketTextChannel); + } + } + /// - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { _client.Log -= LogAsync; + return ValueTask.CompletedTask; } }