mirror of https://github.com/oliverbooth/VPLink
feat: add support for Discord message replies
This commit is contained in:
parent
02287d4995
commit
eef18ec1d3
|
@ -18,4 +18,16 @@ public interface IChatConfiguration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The font style.</value>
|
/// <value>The font style.</value>
|
||||||
FontStyle Style { get; set; }
|
FontStyle Style { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the color of a reply message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The reply message color.</value>
|
||||||
|
uint ReplyColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the font style of a reply message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The reply font style.</value>
|
||||||
|
FontStyle ReplyStyle { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,14 @@ public struct PlainTextMessageBuilder : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the builder.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_builder.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,12 +10,20 @@ public readonly struct RelayedMessage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="author">The author.</param>
|
/// <param name="author">The author.</param>
|
||||||
/// <param name="content">The content.</param>
|
/// <param name="content">The content.</param>
|
||||||
public RelayedMessage(string author, string content)
|
/// <param name="isReply">A value indicating whether this message is a reply.</param>
|
||||||
|
public RelayedMessage(string? author, string content, bool isReply)
|
||||||
{
|
{
|
||||||
Author = author;
|
Author = author;
|
||||||
Content = content;
|
Content = content;
|
||||||
|
IsReply = isReply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user that sent the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The user that sent the message.</value>
|
||||||
|
public string? Author { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the message content.
|
/// Gets the message content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -23,8 +31,8 @@ public readonly struct RelayedMessage
|
||||||
public string Content { get; }
|
public string Content { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user that sent the message.
|
/// Gets a value indicating whether this message is a reply.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The user that sent the message.</value>
|
/// <value><see langword="true" /> if this message is a reply; otherwise, <see langword="false" />.</value>
|
||||||
public string Author { get; }
|
public bool IsReply { get; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace VPLink.Common.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for the <see cref="IUser" /> interface.
|
||||||
|
/// </summary>
|
||||||
|
public static class UserExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the display name of the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>The display name.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,4 +11,10 @@ internal sealed class ChatConfiguration : IChatConfiguration
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public FontStyle Style { get; set; } = FontStyle.Regular;
|
public FontStyle Style { get; set; } = FontStyle.Regular;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public uint ReplyColor { get; set; } = 0x808080;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FontStyle ReplyStyle { get; set; } = FontStyle.Italic;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||||
using VPLink.Common;
|
using VPLink.Common;
|
||||||
using VPLink.Common.Configuration;
|
using VPLink.Common.Configuration;
|
||||||
using VPLink.Common.Data;
|
using VPLink.Common.Data;
|
||||||
|
using VPLink.Common.Extensions;
|
||||||
using VPLink.Common.Services;
|
using VPLink.Common.Services;
|
||||||
using VpSharp.Entities;
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
@ -88,45 +89,106 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage
|
||||||
if (!ValidateMessage(arg, out IUserMessage? message, out IGuildUser? author))
|
if (!ValidateMessage(arg, out IUserMessage? message, out IGuildUser? author))
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
string displayName = author.Nickname ?? author.GlobalName ?? author.Username;
|
string displayName = author.GetDisplayName();
|
||||||
var builder = new PlainTextMessageBuilder();
|
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();
|
var content = builder.ToString();
|
||||||
|
|
||||||
_logger.LogInformation("Message by {Author}: {Content}", author, content);
|
_logger.LogInformation("Message by {Author}: {Content}", author, content);
|
||||||
|
|
||||||
Span<byte> buffer = stackalloc byte[255]; // VP message length limit
|
|
||||||
var messages = new List<RelayedMessage>();
|
var messages = new List<RelayedMessage>();
|
||||||
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<IAttachment> 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<RelayedMessage> messages, string displayName, string content)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[255]; // VP message length limit
|
||||||
|
int byteCount = Utf8Encoding.GetByteCount(content);
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
while (offset < byteCount)
|
while (offset < byteCount)
|
||||||
{
|
{
|
||||||
int length = Math.Min(byteCount - offset, 255);
|
int length = Math.Min(byteCount - offset, 255);
|
||||||
Utf8Encoding.GetBytes(content.AsSpan(offset, length), buffer);
|
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;
|
offset += length;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyCollection<IAttachment> 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,
|
private string? GetReplyContent(SocketMessage message, MessageReference reference, out IUserMessage? fetchedMessage)
|
||||||
ReadOnlySpan<char> content,
|
|
||||||
ref PlainTextMessageBuilder builder,
|
|
||||||
ref Utf8ValueStringBuilder wordBuffer)
|
|
||||||
{
|
{
|
||||||
|
fetchedMessage = null;
|
||||||
|
IGuild authorGuild = ((IGuildUser)message.Author).Guild;
|
||||||
|
IGuild guild = authorGuild;
|
||||||
|
|
||||||
|
Optional<ulong> referenceGuildId = reference.GuildId;
|
||||||
|
Optional<ulong> 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<char> content, ref PlainTextMessageBuilder builder)
|
||||||
|
{
|
||||||
|
Utf8ValueStringBuilder wordBuffer = ZString.CreateUtf8StringBuilder();
|
||||||
|
|
||||||
for (var index = 0; index < content.Length; index++)
|
for (var index = 0; index < content.Length; index++)
|
||||||
{
|
{
|
||||||
char current = content[index];
|
char current = content[index];
|
||||||
|
@ -145,6 +207,8 @@ internal sealed class DiscordMessageService : BackgroundService, IDiscordMessage
|
||||||
{
|
{
|
||||||
AddWord(guild, ref builder, ref wordBuffer, '\0');
|
AddWord(guild, ref builder, ref wordBuffer, '\0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wordBuffer.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddWord(IGuild guild,
|
private static void AddWord(IGuild guild,
|
||||||
|
|
|
@ -45,8 +45,8 @@ internal sealed class VirtualParadiseMessageService : BackgroundService, IVirtua
|
||||||
{
|
{
|
||||||
IChatConfiguration configuration = _configurationService.VirtualParadiseConfiguration.Chat;
|
IChatConfiguration configuration = _configurationService.VirtualParadiseConfiguration.Chat;
|
||||||
|
|
||||||
Color color = Color.FromArgb((int)configuration.Color);
|
Color color = Color.FromArgb((int)(message.IsReply ? configuration.ReplyColor : configuration.Color));
|
||||||
FontStyle style = configuration.Style;
|
FontStyle style = message.IsReply ? configuration.ReplyStyle : configuration.Style;
|
||||||
|
|
||||||
string content = Format.StripMarkDown(message.Content);
|
string content = Format.StripMarkDown(message.Content);
|
||||||
return _virtualParadiseClient.SendMessageAsync(message.Author, content, style, color);
|
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);
|
_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);
|
_messageReceived.OnNext(relayedMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue