From 43f04653632f84c07172b1412053a13b3f066e23 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 20:58:07 +0100 Subject: [PATCH 01/20] chore: bump to 1.3.0 --- VPLink/VPLink.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VPLink/VPLink.csproj b/VPLink/VPLink.csproj index 7b45bd8..e3a7140 100644 --- a/VPLink/VPLink.csproj +++ b/VPLink/VPLink.csproj @@ -9,7 +9,7 @@ Oliver Booth https://github.com/oliverbooth/VpBridge git - 1.2.1 + 1.3.0 From ce0a49350b77b7bfd90bdebba85f548fdbdb34b6 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:22:40 +0100 Subject: [PATCH 02/20] feat: honour Discord nickname, if one is set --- VPLink/Services/DiscordMessageService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index d50bd8e..83126e0 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -97,7 +97,9 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor if (arg is not IUserMessage message) return Task.CompletedTask; - IUser author = message.Author; + if (message.Author is not IGuildUser author) + return Task.CompletedTask; + if (author.Id == _discordClient.CurrentUser.Id) return Task.CompletedTask; @@ -107,7 +109,7 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor if (message.Channel.Id != _configurationService.DiscordConfiguration.ChannelId) return Task.CompletedTask; - string displayName = author.GlobalName ?? author.Username; + string displayName = author.Nickname ?? author.GlobalName ?? author.Username; string unescaped = UnescapeRegex.Replace(message.Content, "$1"); string content = EscapeRegex.Replace(unescaped, "\\$1"); From e293b9b67cef879c7afd46bab9ccaf49b2c6111d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:23:04 +0100 Subject: [PATCH 03/20] style: decrease avatar event embed size (resolves #1) --- VPLink/Services/DiscordMessageService.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 83126e0..4d6a1db 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -54,10 +54,7 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor var embed = new EmbedBuilder(); embed.WithColor(0x00FF00); - embed.WithTitle("📥 Avatar Joined"); - embed.WithDescription(avatar.Name); - embed.WithTimestamp(DateTimeOffset.UtcNow); - embed.WithFooter($"Session {avatar.Session}"); + embed.WithDescription($"📥 **Avatar Joined**: {avatar.Name} (User #{avatar.User.Id})"); return channel.SendMessageAsync(embed: embed.Build()); } @@ -70,10 +67,7 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor var embed = new EmbedBuilder(); embed.WithColor(0xFF0000); - embed.WithTitle("📤 Avatar Left"); - embed.WithDescription(avatar.Name); - embed.WithTimestamp(DateTimeOffset.UtcNow); - embed.WithFooter($"Session {avatar.Session}"); + embed.WithDescription($"📤 **Avatar Left**: {avatar.Name} (User #{avatar.User.Id})"); return channel.SendMessageAsync(embed: embed.Build()); } From 25d725e1b3f61c4e8a640c32c06fa41a3d9c035e Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:31:29 +0100 Subject: [PATCH 04/20] feat: add /info command (resolves #2) --- VPLink/Commands/InfoCommand.cs | 57 +++++++++++++++++++++++++++++ VPLink/Program.cs | 2 + VPLink/Services/BotService.cs | 61 +++++++++++++++++++++++++++++++ VPLink/Services/DiscordService.cs | 1 + 4 files changed, 121 insertions(+) create mode 100644 VPLink/Commands/InfoCommand.cs create mode 100644 VPLink/Services/BotService.cs diff --git a/VPLink/Commands/InfoCommand.cs b/VPLink/Commands/InfoCommand.cs new file mode 100644 index 0000000..6f34be0 --- /dev/null +++ b/VPLink/Commands/InfoCommand.cs @@ -0,0 +1,57 @@ +using System.Text; +using Discord; +using Discord.Interactions; +using Discord.WebSocket; +using VPLink.Services; + +namespace VPLink.Commands; + +/// +/// Represents a class which implements the info command. +/// +internal sealed class InfoCommand : InteractionModuleBase +{ + private readonly BotService _botService; + private readonly DiscordSocketClient _discordClient; + + /// + /// Initializes a new instance of the class. + /// + /// The bot service. + /// + public InfoCommand(BotService botService, DiscordSocketClient discordClient) + { + _botService = botService; + _discordClient = discordClient; + } + + [SlashCommand("info", "Displays information about the bot.")] + [RequireContext(ContextType.Guild)] + public async Task InfoAsync() + { + SocketGuildUser member = Context.Guild.GetUser(_discordClient.CurrentUser.Id); + string pencilVersion = _botService.Version; + + SocketRole? highestRole = member.Roles.Where(r => r.Color != Color.Default).MaxBy(r => r.Position); + + var embed = new EmbedBuilder(); + embed.WithAuthor(member); + embed.WithColor(highestRole?.Color ?? Color.Default); + embed.WithThumbnailUrl(member.GetAvatarUrl()); + embed.WithTitle($"VPLink v{pencilVersion}"); + embed.WithDescription("Created by <@94248427663130624>, hosted [on GitHub](https://github.com/oliverbooth/VPLink)."); + embed.AddField("Ping", $"{_discordClient.Latency} ms", true); + embed.AddField("Started", $"", true); + + var builder = new StringBuilder(); + builder.AppendLine($"VPLink: {pencilVersion}"); + builder.AppendLine($"Discord.Net: {_botService.DiscordNetVersion}"); + builder.AppendLine($"VP#: {_botService.VpSharpVersion}"); + builder.AppendLine($"CLR: {Environment.Version.ToString(3)}"); + builder.AppendLine($"Host: {Environment.OSVersion}"); + + embed.AddField("Version", $"```\n{builder}\n```"); + + await RespondAsync(embed: embed.Build(), ephemeral: true).ConfigureAwait(false); + } +} diff --git a/VPLink/Program.cs b/VPLink/Program.cs index 2cb9a15..621a79f 100644 --- a/VPLink/Program.cs +++ b/VPLink/Program.cs @@ -25,6 +25,8 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(); +builder.Services.AddHostedSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/VPLink/Services/BotService.cs b/VPLink/Services/BotService.cs new file mode 100644 index 0000000..52c558e --- /dev/null +++ b/VPLink/Services/BotService.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using Discord.WebSocket; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using VpSharp; + +namespace VPLink.Services; + +internal sealed class BotService : BackgroundService +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public BotService(ILogger logger) + { + _logger = logger; + var attribute = typeof(BotService).Assembly.GetCustomAttribute(); + Version = attribute?.InformationalVersion ?? "Unknown"; + + attribute = typeof(DiscordSocketClient).Assembly.GetCustomAttribute(); + DiscordNetVersion = attribute?.InformationalVersion ?? "Unknown"; + + attribute = typeof(VirtualParadiseClient).Assembly.GetCustomAttribute(); + VpSharpVersion = attribute?.InformationalVersion ?? "Unknown"; + } + + /// + /// Gets the Discord.Net version. + /// + /// The Discord.Net version. + public string DiscordNetVersion { get; } + + /// + /// Gets the date and time at which the bot was started. + /// + /// The start timestamp. + public DateTimeOffset StartedAt { get; private set; } + + /// + /// Gets the bot version. + /// + /// The bot version. + public string Version { get; } + + /// + /// Gets the VP# version. + /// + /// The VP# version. + public string VpSharpVersion { get; } + + /// + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + StartedAt = DateTimeOffset.UtcNow; + _logger.LogInformation("VPLink v{Version} is starting...", Version); + return Task.CompletedTask; + } +} diff --git a/VPLink/Services/DiscordService.cs b/VPLink/Services/DiscordService.cs index 530bf02..1ba757d 100644 --- a/VPLink/Services/DiscordService.cs +++ b/VPLink/Services/DiscordService.cs @@ -44,6 +44,7 @@ internal sealed class DiscordService : BackgroundService _logger.LogInformation("Establishing relay"); _logger.LogInformation("Adding command modules"); + await _interactionService.AddModuleAsync(_serviceProvider).ConfigureAwait(false); await _interactionService.AddModuleAsync(_serviceProvider).ConfigureAwait(false); _discordClient.Ready += OnReady; From aaacb3027b76b2bc71f123b6fea254d6f5deb1fb Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:37:30 +0100 Subject: [PATCH 05/20] refactor: display user id, not session id, in /who command --- VPLink/Commands/WhoCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VPLink/Commands/WhoCommand.cs b/VPLink/Commands/WhoCommand.cs index 2f23e63..401a96c 100644 --- a/VPLink/Commands/WhoCommand.cs +++ b/VPLink/Commands/WhoCommand.cs @@ -41,12 +41,12 @@ internal sealed class WhoCommand : InteractionModuleBase Date: Sat, 26 Aug 2023 21:38:15 +0100 Subject: [PATCH 06/20] feat: print discord.net and vp# versions on startup --- VPLink/Services/BotService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/VPLink/Services/BotService.cs b/VPLink/Services/BotService.cs index 52c558e..aea7cf7 100644 --- a/VPLink/Services/BotService.cs +++ b/VPLink/Services/BotService.cs @@ -56,6 +56,8 @@ internal sealed class BotService : BackgroundService { StartedAt = DateTimeOffset.UtcNow; _logger.LogInformation("VPLink v{Version} is starting...", Version); + _logger.LogInformation("Discord.Net v{Version}", DiscordNetVersion); + _logger.LogInformation("VP# v{Version}", VpSharpVersion); return Task.CompletedTask; } } From 03ced40ad6b3e7ccfe02d1ea96b37fe785fcfcf4 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:38:36 +0100 Subject: [PATCH 07/20] style: remove unused ns import --- VPLink/Services/DiscordMessageService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 4d6a1db..4d25930 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -12,7 +12,6 @@ using VPLink.Common.Configuration; using VPLink.Common.Data; using VPLink.Common.Services; using VpSharp.Entities; -using IUser = Discord.IUser; namespace VPLink.Services; From a5484365e45fb7a22384fc529b68eaa527936fed Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:41:41 +0100 Subject: [PATCH 08/20] feat: log user from avatar events --- VPLink/Services/AvatarService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VPLink/Services/AvatarService.cs b/VPLink/Services/AvatarService.cs index f2d5a80..7850aeb 100644 --- a/VPLink/Services/AvatarService.cs +++ b/VPLink/Services/AvatarService.cs @@ -49,7 +49,7 @@ internal sealed class AvatarService : BackgroundService, IAvatarService private void OnVPAvatarJoined(VirtualParadiseAvatar avatar) { - _logger.LogInformation("{Avatar} joined", avatar); + _logger.LogInformation("{Avatar} joined ({User})", avatar, avatar.User); IBotConfiguration configuration = _configurationService.BotConfiguration; if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots) @@ -60,7 +60,7 @@ internal sealed class AvatarService : BackgroundService, IAvatarService private void OnVPAvatarLeft(VirtualParadiseAvatar avatar) { - _logger.LogInformation("{Avatar} left", avatar); + _logger.LogInformation("{Avatar} left ({User})", avatar, avatar.User); IBotConfiguration configuration = _configurationService.BotConfiguration; if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots) From cb3c08ffe3023d9051be117d1246028e915ec49a Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 21:46:58 +0100 Subject: [PATCH 09/20] feat: suppress repeated event from same user --- VPLink/Services/AvatarService.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/VPLink/Services/AvatarService.cs b/VPLink/Services/AvatarService.cs index 7850aeb..8f90ef2 100644 --- a/VPLink/Services/AvatarService.cs +++ b/VPLink/Services/AvatarService.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Extensions.Hosting; @@ -17,6 +18,7 @@ internal sealed class AvatarService : BackgroundService, IAvatarService private readonly VirtualParadiseClient _virtualParadiseClient; private readonly Subject _avatarJoined = new(); private readonly Subject _avatarLeft = new(); + private readonly ConcurrentDictionary _cachedAvatars = new(); /// /// Initializes a new instance of the class. @@ -55,7 +57,8 @@ internal sealed class AvatarService : BackgroundService, IAvatarService if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots) return; - _avatarJoined.OnNext(avatar); + if (AddCachedAvatar(avatar)) + _avatarJoined.OnNext(avatar); } private void OnVPAvatarLeft(VirtualParadiseAvatar avatar) @@ -66,6 +69,24 @@ internal sealed class AvatarService : BackgroundService, IAvatarService if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots) return; - _avatarLeft.OnNext(avatar); + if (RemoveCachedAvatar(avatar)) + _avatarLeft.OnNext(avatar); + } + + private bool AddCachedAvatar(VirtualParadiseAvatar avatar) + { + if (avatar is null) throw new ArgumentNullException(nameof(avatar)); + + bool result = !_cachedAvatars.Values.Any(a => a.User.Id == avatar.User.Id && a.Name == avatar.Name); + _cachedAvatars[avatar.Session] = avatar; + return result; + } + + private bool RemoveCachedAvatar(VirtualParadiseAvatar avatar) + { + if (avatar is null) throw new ArgumentNullException(nameof(avatar)); + + _cachedAvatars.TryRemove(avatar.Session, out _); + return !_cachedAvatars.Values.Any(a => a.User.Id == avatar.User.Id && a.Name == avatar.Name); } } From 70708e3eef71584a50068bcb07084530d2cf1ffa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 26 Aug 2023 22:39:03 +0100 Subject: [PATCH 10/20] feat: strip markdown from Discord-sent messages --- VPLink/Services/VirtualParadiseMessageService.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/VPLink/Services/VirtualParadiseMessageService.cs b/VPLink/Services/VirtualParadiseMessageService.cs index be7c784..8d61eac 100644 --- a/VPLink/Services/VirtualParadiseMessageService.cs +++ b/VPLink/Services/VirtualParadiseMessageService.cs @@ -1,6 +1,6 @@ -using System.Drawing; using System.Reactive.Linq; using System.Reactive.Subjects; +using Discord; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using VPLink.Common.Configuration; @@ -8,7 +8,9 @@ using VPLink.Common.Data; using VPLink.Common.Services; using VpSharp; using VpSharp.Entities; +using Color = System.Drawing.Color; using FontStyle = VpSharp.FontStyle; +using MessageType = VpSharp.MessageType; namespace VPLink.Services; @@ -45,7 +47,9 @@ internal sealed class VirtualParadiseMessageService : BackgroundService, IVirtua Color color = Color.FromArgb((int)configuration.Color); FontStyle style = configuration.Style; - return _virtualParadiseClient.SendMessageAsync(message.Author, message.Content, style, color); + + string content = Format.StripMarkDown(message.Content); + return _virtualParadiseClient.SendMessageAsync(message.Author, content, style, color); } /// From 64fd5d45bf052466bf37d4651940f92a8e7be05a Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 00:19:02 +0100 Subject: [PATCH 11/20] perf: don't escape markdown with regex --- VPLink/Services/DiscordMessageService.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 4d25930..57ad469 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text; -using System.Text.RegularExpressions; using Cysharp.Text; using Discord; using Discord.WebSocket; @@ -19,8 +18,6 @@ namespace VPLink.Services; internal sealed partial class DiscordMessageService : BackgroundService, IDiscordMessageService { private static readonly Encoding Utf8Encoding = new UTF8Encoding(false, false); - private static readonly Regex UnescapeRegex = GetUnescapeRegex(); - private static readonly Regex EscapeRegex = GetEscapeRegex(); private readonly ILogger _logger; private readonly IConfigurationService _configurationService; @@ -103,8 +100,7 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor return Task.CompletedTask; string displayName = author.Nickname ?? author.GlobalName ?? author.Username; - string unescaped = UnescapeRegex.Replace(message.Content, "$1"); - string content = EscapeRegex.Replace(unescaped, "\\$1"); + string content = message.Content; IReadOnlyCollection attachments = message.Attachments; if (attachments.Count > 0) @@ -154,10 +150,4 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor channel = null; return false; } - - [GeneratedRegex(@"\\(\*|_|`|~|\\)", RegexOptions.Compiled)] - private static partial Regex GetUnescapeRegex(); - - [GeneratedRegex(@"(\*|_|`|~|\\)", RegexOptions.Compiled)] - private static partial Regex GetEscapeRegex(); } From 9edd4b81ce4c5d5e3bd5432132cdcdbb43b2b2fb Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 00:19:43 +0100 Subject: [PATCH 12/20] style: move message validation to own method --- VPLink/Services/DiscordMessageService.cs | 56 ++++++++++++++++++------ 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 57ad469..4b4f54f 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -84,19 +84,7 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor private Task OnDiscordMessageReceived(SocketMessage arg) { - if (arg is not IUserMessage message) - return Task.CompletedTask; - - if (message.Author is not IGuildUser author) - return Task.CompletedTask; - - if (author.Id == _discordClient.CurrentUser.Id) - return Task.CompletedTask; - - if (author.IsBot && !_configurationService.BotConfiguration.RelayBotMessages) - return Task.CompletedTask; - - if (message.Channel.Id != _configurationService.DiscordConfiguration.ChannelId) + if (!ValidateMessage(arg, out IUserMessage? message, out IGuildUser? author)) return Task.CompletedTask; string displayName = author.Nickname ?? author.GlobalName ?? author.Username; @@ -150,4 +138,46 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor channel = null; return false; } + + private bool ValidateMessage(SocketMessage socketMessage, + [NotNullWhen(true)] out IUserMessage? message, + [NotNullWhen(true)] out IGuildUser? author) + { + message = socketMessage as IUserMessage; + if (message is null) + { + author = null; + return false; + } + + author = message.Author as IGuildUser; + if (author is null) + { + message = null; + return false; + } + + if (author.Id == _discordClient.CurrentUser.Id) + { + author = null; + message = null; + return false; + } + + if (author.IsBot && !_configurationService.BotConfiguration.RelayBotMessages) + { + author = null; + message = null; + return false; + } + + if (message.Channel.Id != _configurationService.DiscordConfiguration.ChannelId) + { + author = null; + message = null; + return false; + } + + return true; + } } From 47d5323597e066b61a3d0cb1fc297ddca0fa14c2 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 00:22:39 +0100 Subject: [PATCH 13/20] fix: ignore contentless and attachmentless messages --- VPLink/Services/DiscordMessageService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 4b4f54f..0bc31bd 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -178,6 +178,13 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor return false; } + if (string.IsNullOrWhiteSpace(message.Content) && message.Attachments.Count == 0) + { + author = null; + message = null; + return false; + } + return true; } } From 02287d499563948b609cd0b46c6237ae85b7f3f1 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 14:35:36 +0100 Subject: [PATCH 14/20] feat: add support for Discord's formatting --- VPLink.Common/Data/PlainTextMessageBuilder.cs | 99 +++++++++++++ VPLink.Common/Data/TimestampFormat.cs | 13 ++ VPLink.Common/MentionUtility.cs | 115 +++++++++++++++ VPLink.Common/VPLink.Common.csproj | 2 + VPLink/Services/DiscordMessageService.cs | 135 ++++++++++++++++-- VPLink/VPLink.csproj | 3 +- 6 files changed, 350 insertions(+), 17 deletions(-) create mode 100644 VPLink.Common/Data/PlainTextMessageBuilder.cs create mode 100644 VPLink.Common/Data/TimestampFormat.cs create mode 100644 VPLink.Common/MentionUtility.cs diff --git a/VPLink.Common/Data/PlainTextMessageBuilder.cs b/VPLink.Common/Data/PlainTextMessageBuilder.cs new file mode 100644 index 0000000..7c039a5 --- /dev/null +++ b/VPLink.Common/Data/PlainTextMessageBuilder.cs @@ -0,0 +1,99 @@ +using Cysharp.Text; +using Humanizer; + +namespace VPLink.Common.Data; + +/// +/// Represents a plain text message builder. +/// +public struct PlainTextMessageBuilder : IDisposable +{ + private Utf8ValueStringBuilder _builder; + + /// + /// Initializes a new instance of the struct. + /// + public PlainTextMessageBuilder() + { + _builder = ZString.CreateUtf8StringBuilder(); + } + + /// + /// Appends the specified word. + /// + /// The word. + /// The trailing whitespace trivia. + public void AddWord(ReadOnlySpan word, char whitespace = ' ') + { + _builder.Append(word); + if (whitespace != '\0') _builder.Append(whitespace); + } + + /// + /// Appends the specified word. + /// + /// The timestamp. + /// The format. + /// The trailing whitespace trivia. + public void AddTimestamp(DateTimeOffset timestamp, TimestampFormat format = TimestampFormat.None, + char whitespace = ' ') + { + switch (format) + { + case TimestampFormat.Relative: + AddWord(timestamp.Humanize(), whitespace); + break; + + case TimestampFormat.None: + AddWord(timestamp.ToString("d MMM yyyy HH:mm")); + AddWord(" UTC", whitespace); + break; + + case TimestampFormat.LongDate: + AddWord(timestamp.ToString("dd MMMM yyyy")); + AddWord(" UTC", whitespace); + break; + + case TimestampFormat.ShortDate: + AddWord(timestamp.ToString("dd/MM/yyyy")); + AddWord(" UTC", whitespace); + break; + + case TimestampFormat.ShortTime: + AddWord(timestamp.ToString("HH:mm")); + AddWord("UTC", whitespace); + break; + + case TimestampFormat.LongTime: + AddWord(timestamp.ToString("HH:mm:ss")); + AddWord(" UTC", whitespace); + break; + + case TimestampFormat.ShortDateTime: + AddWord(timestamp.ToString("dd MMMM yyyy HH:mm")); + AddWord(" UTC", whitespace); + break; + + case TimestampFormat.LongDateTime: + AddWord(timestamp.ToString("dddd, dd MMMM yyyy HH:mm")); + AddWord(" UTC", whitespace); + break; + + default: + AddWord($"", whitespace); + break; + } + } + + /// + public void Dispose() + { + _builder.Dispose(); + } + + /// + public override string ToString() + { + return _builder.ToString().Trim(); + } +} diff --git a/VPLink.Common/Data/TimestampFormat.cs b/VPLink.Common/Data/TimestampFormat.cs new file mode 100644 index 0000000..f6091d4 --- /dev/null +++ b/VPLink.Common/Data/TimestampFormat.cs @@ -0,0 +1,13 @@ +namespace VPLink.Common.Data; + +public enum TimestampFormat +{ + None = '\0', + ShortTime = 't', + LongTime = 'T', + ShortDate = 'd', + LongDate = 'D', + ShortDateTime = 'f', + LongDateTime = 'F', + Relative = 'R' +} diff --git a/VPLink.Common/MentionUtility.cs b/VPLink.Common/MentionUtility.cs new file mode 100644 index 0000000..ebe9963 --- /dev/null +++ b/VPLink.Common/MentionUtility.cs @@ -0,0 +1,115 @@ +using System.Globalization; +using System.Text; +using Cysharp.Text; +using Discord; +using VPLink.Common.Data; + +namespace VPLink.Common; + +public static class MentionUtility +{ + public static void ParseTag(IGuild guild, + ReadOnlySpan contents, + ref PlainTextMessageBuilder builder, + char whitespaceTrivia) + { + if (contents[..2].Equals("@&", StringComparison.Ordinal)) // role mention + { + ParseRoleMention(guild, contents, ref builder, whitespaceTrivia); + } + else if (contents[..2].Equals("t:", StringComparison.Ordinal)) // timestamp + { + ParseTimestamp(contents, ref builder, whitespaceTrivia); + } + else + switch (contents[0]) + { + // user mention + case '@': + ParseUserMention(guild, contents, ref builder, whitespaceTrivia); + break; + + // channel mention + case '#': + ParseChannelMention(guild, contents, ref builder, whitespaceTrivia); + break; + + default: + builder.AddWord($"<{contents.ToString()}>", whitespaceTrivia); + break; + } + } + + private static void ParseChannelMention(IGuild guild, + ReadOnlySpan contents, + ref PlainTextMessageBuilder builder, + char whitespaceTrivia) + { + ulong channelId = ulong.Parse(contents[1..]); + ITextChannel? channel = guild.GetTextChannelAsync(channelId).GetAwaiter().GetResult(); + builder.AddWord(channel is null ? $"<{contents}>" : $"#{channel.Name}", whitespaceTrivia); + } + + private static void ParseRoleMention(IGuild guild, + ReadOnlySpan contents, + ref PlainTextMessageBuilder builder, + char whitespaceTrivia) + { + ulong roleId = ulong.Parse(contents[2..]); + IRole? role = guild.GetRole(roleId); + builder.AddWord(role is null ? $"<{contents}>" : $"@{role.Name}", whitespaceTrivia); + } + + private static void ParseUserMention(IGuild guild, + ReadOnlySpan contents, + ref PlainTextMessageBuilder builder, + char whitespaceTrivia) + { + ulong userId = ulong.Parse(contents[1..]); + IGuildUser? user = guild.GetUserAsync(userId).GetAwaiter().GetResult(); + builder.AddWord(user is null ? $"<{contents}>" : $"@{user.Nickname ?? user.GlobalName ?? user.Username}", + whitespaceTrivia); + } + + private static void ParseTimestamp(ReadOnlySpan contents, + ref PlainTextMessageBuilder builder, + char whitespaceTrivia) + { + using Utf8ValueStringBuilder buffer = ZString.CreateUtf8StringBuilder(); + var formatSpecifier = '\0'; + var isEscaped = false; + + for (var index = 2; index < contents.Length; index++) + { + char current = contents[index]; + switch (current) + { + case '\\': + isEscaped = !isEscaped; + break; + + case ':' when !isEscaped && index + 1 < contents.Length: + formatSpecifier = contents[index + 1]; + break; + + case '>' when !isEscaped: + break; + + case var _ when char.IsDigit(current): + buffer.Append(current); + break; + + default: + return; + } + } + + ReadOnlySpan bytes = buffer.AsSpan(); + int charCount = Encoding.UTF8.GetCharCount(bytes); + Span chars = stackalloc char[charCount]; + Encoding.UTF8.GetChars(bytes, chars); + + DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(chars, CultureInfo.InvariantCulture)); + builder.AddTimestamp(timestamp, (TimestampFormat)formatSpecifier, whitespaceTrivia); + } +} diff --git a/VPLink.Common/VPLink.Common.csproj b/VPLink.Common/VPLink.Common.csproj index 4c50db9..d052804 100644 --- a/VPLink.Common/VPLink.Common.csproj +++ b/VPLink.Common/VPLink.Common.csproj @@ -7,6 +7,8 @@ + + diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index 0bc31bd..46b5eea 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -7,6 +7,7 @@ using Discord; using Discord.WebSocket; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using VPLink.Common; using VPLink.Common.Configuration; using VPLink.Common.Data; using VPLink.Common.Services; @@ -15,7 +16,7 @@ using VpSharp.Entities; namespace VPLink.Services; /// -internal sealed partial class DiscordMessageService : BackgroundService, IDiscordMessageService +internal sealed class DiscordMessageService : BackgroundService, IDiscordMessageService { private static readonly Encoding Utf8Encoding = new UTF8Encoding(false, false); @@ -88,22 +89,12 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor return Task.CompletedTask; string displayName = author.Nickname ?? author.GlobalName ?? author.Username; - string content = message.Content; + var builder = new PlainTextMessageBuilder(); + Utf8ValueStringBuilder wordBuffer = ZString.CreateUtf8StringBuilder(); - IReadOnlyCollection attachments = message.Attachments; - if (attachments.Count > 0) - { - using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); - for (var index = 0; index < attachments.Count; index++) - { - builder.AppendLine(attachments.ElementAt(index).Url); - } + SanitizeContent(author.Guild, message.Content, ref builder, ref wordBuffer); + var content = builder.ToString(); - // += allocates more than necessary, just interpolate - content = $"{content}\n{builder}"; - } - - content = content.Trim(); _logger.LogInformation("Message by {Author}: {Content}", author, content); Span buffer = stackalloc byte[255]; // VP message length limit @@ -119,10 +110,91 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor 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) + { + for (var index = 0; index < content.Length; index++) + { + char current = content[index]; + if (char.IsWhiteSpace(current)) + { + AddWord(guild, ref builder, ref wordBuffer, current); + wordBuffer.Clear(); + } + else + { + wordBuffer.Append(current); + } + } + + if (wordBuffer.Length > 0) + { + AddWord(guild, ref builder, ref wordBuffer, '\0'); + } + } + + private static void AddWord(IGuild guild, + ref PlainTextMessageBuilder builder, + ref Utf8ValueStringBuilder wordBuffer, + char whitespaceTrivia) + { + using Utf8ValueStringBuilder buffer = ZString.CreateUtf8StringBuilder(); + + ReadOnlySpan bytes = wordBuffer.AsSpan(); + int charCount = Utf8Encoding.GetCharCount(bytes); + Span chars = stackalloc char[charCount]; + Utf8Encoding.GetChars(bytes, chars); + + Span temp = stackalloc char[255]; + + var isEscaped = false; + for (var index = 0; index < chars.Length; index++) + { + char current = chars[index]; + switch (current) + { + case '\\' when isEscaped: + buffer.Append('\\'); + break; + + case '\\': + isEscaped = !isEscaped; + break; + + case '<': + index++; + int tagLength = ConsumeToEndOfTag(chars, ref index, temp); + char whitespace = index < chars.Length - 1 && char.IsWhiteSpace(chars[index]) ? chars[index] : '\0'; + MentionUtility.ParseTag(guild, temp[..tagLength], ref builder, whitespace); + break; + + default: + buffer.Append(current); + break; + } + } + + bytes = buffer.AsSpan(); + charCount = Utf8Encoding.GetCharCount(bytes); + chars = stackalloc char[charCount]; + Utf8Encoding.GetChars(bytes, chars); + builder.AddWord(chars, whitespaceTrivia); + } + private bool TryGetRelayChannel([NotNullWhen(true)] out ITextChannel? channel) { IDiscordConfiguration configuration = _configurationService.DiscordConfiguration; @@ -187,4 +259,37 @@ internal sealed partial class DiscordMessageService : BackgroundService, IDiscor return true; } + + private static int ConsumeToEndOfTag(ReadOnlySpan word, ref int index, Span element) + { + using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); + var isEscaped = false; + + int startIndex = index; + for (; index < word.Length; index++) + { + switch (word[index]) + { + case '\\' when isEscaped: + builder.Append('\\'); + isEscaped = false; + break; + + case '\\': + isEscaped = true; + break; + + case '>' when !isEscaped: + Utf8Encoding.GetChars(builder.AsSpan(), element); + return index + 1 - startIndex; + + default: + builder.Append(word[index]); + break; + } + } + + Utf8Encoding.GetChars(builder.AsSpan(), element); + return index + 1 - startIndex; + } } diff --git a/VPLink/VPLink.csproj b/VPLink/VPLink.csproj index e3a7140..4d33764 100644 --- a/VPLink/VPLink.csproj +++ b/VPLink/VPLink.csproj @@ -41,7 +41,6 @@ - @@ -56,7 +55,7 @@ - + From eef18ec1d3509e62b7c7452a02a6f79824bc2ca8 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 16:50:17 +0100 Subject: [PATCH 15/20] 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); } } From a49b065ee28bf146d09afcbffbde9820034c1266 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 16:50:33 +0100 Subject: [PATCH 16/20] fix: fix support for Discord timestamps --- VPLink.Common/MentionUtility.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/VPLink.Common/MentionUtility.cs b/VPLink.Common/MentionUtility.cs index ebe9963..5bc355d 100644 --- a/VPLink.Common/MentionUtility.cs +++ b/VPLink.Common/MentionUtility.cs @@ -78,9 +78,15 @@ public static class MentionUtility using Utf8ValueStringBuilder buffer = ZString.CreateUtf8StringBuilder(); var formatSpecifier = '\0'; var isEscaped = false; + var breakLoop = false; for (var index = 2; index < contents.Length; index++) { + if (breakLoop) + { + break; + } + char current = contents[index]; switch (current) { @@ -90,6 +96,8 @@ public static class MentionUtility case ':' when !isEscaped && index + 1 < contents.Length: formatSpecifier = contents[index + 1]; + if (formatSpecifier == '>') formatSpecifier = '\0'; // ignore closing tag + breakLoop = true; break; case '>' when !isEscaped: From 6193e5ddf7a14d6156f778c68ede5859c8568452 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 16:50:44 +0100 Subject: [PATCH 17/20] style: remove redundant space before UTC --- VPLink.Common/Data/PlainTextMessageBuilder.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VPLink.Common/Data/PlainTextMessageBuilder.cs b/VPLink.Common/Data/PlainTextMessageBuilder.cs index 7d9db94..a1f5ff0 100644 --- a/VPLink.Common/Data/PlainTextMessageBuilder.cs +++ b/VPLink.Common/Data/PlainTextMessageBuilder.cs @@ -46,17 +46,17 @@ public struct PlainTextMessageBuilder : IDisposable case TimestampFormat.None: AddWord(timestamp.ToString("d MMM yyyy HH:mm")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; case TimestampFormat.LongDate: AddWord(timestamp.ToString("dd MMMM yyyy")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; case TimestampFormat.ShortDate: AddWord(timestamp.ToString("dd/MM/yyyy")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; case TimestampFormat.ShortTime: @@ -66,17 +66,17 @@ public struct PlainTextMessageBuilder : IDisposable case TimestampFormat.LongTime: AddWord(timestamp.ToString("HH:mm:ss")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; case TimestampFormat.ShortDateTime: AddWord(timestamp.ToString("dd MMMM yyyy HH:mm")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; case TimestampFormat.LongDateTime: AddWord(timestamp.ToString("dddd, dd MMMM yyyy HH:mm")); - AddWord(" UTC", whitespace); + AddWord("UTC", whitespace); break; default: From 1491f7fd868e2a0aeaee39da15bacd27e80567aa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 17:00:30 +0100 Subject: [PATCH 18/20] style: surround bot names with [ ] --- VPLink.Common/Extensions/UserExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/VPLink.Common/Extensions/UserExtensions.cs b/VPLink.Common/Extensions/UserExtensions.cs index fcb5977..868beb0 100644 --- a/VPLink.Common/Extensions/UserExtensions.cs +++ b/VPLink.Common/Extensions/UserExtensions.cs @@ -15,11 +15,13 @@ public static class UserExtensions /// is null. public static string GetDisplayName(this IUser user) { - return user switch + string displayName = user switch { null => throw new ArgumentNullException(nameof(user)), IGuildUser member => member.Nickname ?? member.GlobalName ?? member.Username, _ => user.GlobalName ?? user.Username }; + + return user.IsBot ? $"[{displayName}]" : displayName; } } From 3882d77fbea1e9f19b6d9b29e09b63d5840f5572 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 17:02:30 +0100 Subject: [PATCH 19/20] style: clean up reply branch --- VPLink/Services/DiscordMessageService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index cc68664..abb042d 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -107,11 +107,14 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage if (replyContent is not null) { IUser replyAuthor = fetchedMessage!.Author; + string name = fetchedMessage.Author.GetDisplayName(); + _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!, $"↩️ Replying to {name}:", true)); messages.Add(new RelayedMessage(null!, replyContent, true)); } } From feee8970cae6e5c3c8511cebbd84a447273c62b1 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 27 Aug 2023 17:02:50 +0100 Subject: [PATCH 20/20] feat: add support for app command responses --- VPLink/Services/DiscordMessageService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/VPLink/Services/DiscordMessageService.cs b/VPLink/Services/DiscordMessageService.cs index abb042d..8611e36 100644 --- a/VPLink/Services/DiscordMessageService.cs +++ b/VPLink/Services/DiscordMessageService.cs @@ -119,6 +119,13 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage } } + if (arg.Interaction is { Type: InteractionType.ApplicationCommand } interaction) + { + string name = interaction.User.GetDisplayName(); + string commandName = interaction.Name; + messages.Add(new RelayedMessage(null, $"⌨️ {name} used /{commandName}", true)); + } + AddMessage(messages, displayName, content); IReadOnlyCollection attachments = message.Attachments;