From eef18ec1d3509e62b7c7452a02a6f79824bc2ca8 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 16:50:17 +0100 Subject: [PATCH] feat: add support for Discord message replies --- .../Configuration/IChatConfiguration.cs | 12 ++ VPLink.Common/Data/PlainTextMessageBuilder.cs | 8 ++ VPLink.Common/Data/RelayedMessage.cs | 16 ++- VPLink.Common/Extensions/UserExtensions.cs | 25 +++++ VPLink/Configuration/ChatConfiguration.cs | 6 + VPLink/Services/DiscordMessageService.cs | 106 ++++++++++++++---- .../Services/VirtualParadiseMessageService.cs | 6 +- 7 files changed, 151 insertions(+), 28 deletions(-) create mode 100644 VPLink.Common/Extensions/UserExtensions.cs diff --git a/VPLink.Common/Configuration/IChatConfiguration.cs b/VPLink.Common/Configuration/IChatConfiguration.cs index 86351a5..4edf066 100644 --- a/VPLink.Common/Configuration/IChatConfiguration.cs +++ b/VPLink.Common/Configuration/IChatConfiguration.cs @@ -18,4 +18,16 @@ public interface IChatConfiguration /// /// The font style. FontStyle Style { get; set; } + + /// + /// Gets or sets the color of a reply message. + /// + /// The reply message color. + uint ReplyColor { get; set; } + + /// + /// Gets or sets the font style of a reply message. + /// + /// The reply font style. + FontStyle ReplyStyle { get; set; } } diff --git a/VPLink.Common/Data/PlainTextMessageBuilder.cs b/VPLink.Common/Data/PlainTextMessageBuilder.cs index 7c039a5..7d9db94 100644 --- a/VPLink.Common/Data/PlainTextMessageBuilder.cs +++ b/VPLink.Common/Data/PlainTextMessageBuilder.cs @@ -85,6 +85,14 @@ public struct PlainTextMessageBuilder : IDisposable } } + /// + /// Clears the builder. + /// + public void Clear() + { + _builder.Clear(); + } + /// public void Dispose() { diff --git a/VPLink.Common/Data/RelayedMessage.cs b/VPLink.Common/Data/RelayedMessage.cs index b4f9886..281d96f 100644 --- a/VPLink.Common/Data/RelayedMessage.cs +++ b/VPLink.Common/Data/RelayedMessage.cs @@ -10,12 +10,20 @@ public readonly struct RelayedMessage /// /// The author. /// The content. - public RelayedMessage(string author, string content) + /// A value indicating whether this message is a reply. + public RelayedMessage(string? author, string content, bool isReply) { Author = author; Content = content; + IsReply = isReply; } + /// + /// Gets the user that sent the message. + /// + /// The user that sent the message. + public string? Author { get; } + /// /// Gets the message content. /// @@ -23,8 +31,8 @@ public readonly struct RelayedMessage public string Content { get; } /// - /// Gets the user that sent the message. + /// Gets a value indicating whether this message is a reply. /// - /// The user that sent the message. - public string Author { get; } + /// if this message is a reply; otherwise, . + public bool IsReply { get; } } diff --git a/VPLink.Common/Extensions/UserExtensions.cs b/VPLink.Common/Extensions/UserExtensions.cs new file mode 100644 index 0000000..fcb5977 --- /dev/null +++ b/VPLink.Common/Extensions/UserExtensions.cs @@ -0,0 +1,25 @@ +using Discord; + +namespace VPLink.Common.Extensions; + +/// +/// Provides extension methods for the interface. +/// +public static class UserExtensions +{ + /// + /// Gets the display name of the user. + /// + /// The user. + /// The display name. + /// is null. + public static string GetDisplayName(this IUser user) + { + return user switch + { + null => throw new ArgumentNullException(nameof(user)), + IGuildUser member => member.Nickname ?? member.GlobalName ?? member.Username, + _ => user.GlobalName ?? user.Username + }; + } +} diff --git a/VPLink/Configuration/ChatConfiguration.cs b/VPLink/Configuration/ChatConfiguration.cs index 7fe03cb..f5f6b11 100644 --- a/VPLink/Configuration/ChatConfiguration.cs +++ b/VPLink/Configuration/ChatConfiguration.cs @@ -11,4 +11,10 @@ internal sealed class ChatConfiguration : IChatConfiguration /// public FontStyle Style { get; set; } = FontStyle.Regular; + + /// + public uint ReplyColor { get; set; } = 0x808080; + + /// + public FontStyle ReplyStyle { get; set; } = FontStyle.Italic; } diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 46b5eea..cc68664 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using VPLink.Common; using VPLink.Common.Configuration; using VPLink.Common.Data; +using VPLink.Common.Extensions; using VPLink.Common.Services; using VpSharp.Entities; @@ -88,45 +89,106 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage if (!ValidateMessage(arg, out IUserMessage? message, out IGuildUser? author)) return Task.CompletedTask; - string displayName = author.Nickname ?? author.GlobalName ?? author.Username; + string displayName = author.GetDisplayName(); var builder = new PlainTextMessageBuilder(); - Utf8ValueStringBuilder wordBuffer = ZString.CreateUtf8StringBuilder(); - SanitizeContent(author.Guild, message.Content, ref builder, ref wordBuffer); + IGuild guild = author.Guild; + SanitizeContent(guild, message.Content, ref builder); var content = builder.ToString(); _logger.LogInformation("Message by {Author}: {Content}", author, content); - Span buffer = stackalloc byte[255]; // VP message length limit var messages = new List(); - int byteCount = Utf8Encoding.GetByteCount(content); + MessageReference reference = arg.Reference; + if (reference?.MessageId.IsSpecified == true) + { + string? replyContent = GetReplyContent(arg, reference, out IUserMessage? fetchedMessage); + if (replyContent is not null) + { + IUser replyAuthor = fetchedMessage!.Author; + _logger.LogInformation("Replying to {Author}: {Content}", replyAuthor, replyContent); + builder.Clear(); + SanitizeContent(guild, replyContent, ref builder); + replyContent = builder.ToString(); + messages.Add(new RelayedMessage(null!, $"↩️ Replying to {fetchedMessage.Author.GetDisplayName()}:", true)); + messages.Add(new RelayedMessage(null!, replyContent, true)); + } + } + + AddMessage(messages, displayName, content); + + IReadOnlyCollection attachments = message.Attachments; + foreach (IAttachment attachment in attachments) + { + messages.Add(new RelayedMessage(displayName, attachment.Url, false)); + } + + messages.ForEach(_messageReceived.OnNext); + builder.Dispose(); + return Task.CompletedTask; + } + + private static void AddMessage(ICollection messages, string displayName, string content) + { + Span buffer = stackalloc byte[255]; // VP message length limit + int byteCount = Utf8Encoding.GetByteCount(content); var offset = 0; while (offset < byteCount) { int length = Math.Min(byteCount - offset, 255); Utf8Encoding.GetBytes(content.AsSpan(offset, length), buffer); - messages.Add(new RelayedMessage(displayName, Utf8Encoding.GetString(buffer))); + messages.Add(new RelayedMessage(displayName, Utf8Encoding.GetString(buffer), false)); offset += length; } - - IReadOnlyCollection attachments = message.Attachments; - foreach (IAttachment attachment in attachments) - { - messages.Add(new RelayedMessage(displayName, attachment.Url)); - } - - messages.ForEach(_messageReceived.OnNext); - builder.Dispose(); - wordBuffer.Dispose(); - return Task.CompletedTask; } - private static void SanitizeContent(IGuild guild, - ReadOnlySpan content, - ref PlainTextMessageBuilder builder, - ref Utf8ValueStringBuilder wordBuffer) + private string? GetReplyContent(SocketMessage message, MessageReference reference, out IUserMessage? fetchedMessage) { + fetchedMessage = null; + IGuild authorGuild = ((IGuildUser)message.Author).Guild; + IGuild guild = authorGuild; + + Optional referenceGuildId = reference.GuildId; + Optional referenceMessageId = reference.MessageId; + + if (!referenceMessageId.IsSpecified) + { + return null; + } + + if (referenceGuildId.IsSpecified) + { + guild = _discordClient.GetGuild(referenceGuildId.Value) ?? authorGuild; + } + + ulong referenceChannelId = reference.ChannelId; + + if (!referenceMessageId.IsSpecified) + { + return null; + } + + if (guild.GetChannelAsync(referenceChannelId).GetAwaiter().GetResult() is not ITextChannel channel) + { + return null; + } + + IMessage? referencedMessage = channel.GetMessageAsync(referenceMessageId.Value).GetAwaiter().GetResult(); + if (referencedMessage is null) + { + return null; + } + + fetchedMessage = referencedMessage as IUserMessage; + string? content = referencedMessage.Content; + return string.IsNullOrWhiteSpace(content) ? null : content; + } + + private static void SanitizeContent(IGuild guild, ReadOnlySpan content, ref PlainTextMessageBuilder builder) + { + Utf8ValueStringBuilder wordBuffer = ZString.CreateUtf8StringBuilder(); + for (var index = 0; index < content.Length; index++) { char current = content[index]; @@ -145,6 +207,8 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage { AddWord(guild, ref builder, ref wordBuffer, '\0'); } + + wordBuffer.Dispose(); } private static void AddWord(IGuild guild, diff --git a/VPLink/Services/VirtualParadiseMessageService.cs b/VPLink/Services/VirtualParadiseMessageService.cs index 8d61eac..48f0117 100644 --- a/VPLink/Services/VirtualParadiseMessageService.cs +++ b/VPLink/Services/VirtualParadiseMessageService.cs @@ -45,8 +45,8 @@ internal sealed class VirtualParadiseMessageService : BackgroundService, IVirtua { IChatConfiguration configuration = _configurationService.VirtualParadiseConfiguration.Chat; - Color color = Color.FromArgb((int)configuration.Color); - FontStyle style = configuration.Style; + Color color = Color.FromArgb((int)(message.IsReply ? configuration.ReplyColor : configuration.Color)); + FontStyle style = message.IsReply ? configuration.ReplyStyle : configuration.Style; string content = Format.StripMarkDown(message.Content); return _virtualParadiseClient.SendMessageAsync(message.Author, content, style, color); @@ -68,7 +68,7 @@ internal sealed class VirtualParadiseMessageService : BackgroundService, IVirtua _logger.LogInformation("Message by {Author}: {Content}", message.Author, message.Content); - var relayedMessage = new RelayedMessage(message.Author.Name, message.Content); + var relayedMessage = new RelayedMessage(message.Author.Name, message.Content, false); _messageReceived.OnNext(relayedMessage); } }