Skip to content

Commit

Permalink
Merge branch 'release/1.3.0' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverbooth committed Aug 27, 2023
2 parents e64ebdd + feee897 commit f4b5ff3
Show file tree
Hide file tree
Showing 17 changed files with 706 additions and 60 deletions.
12 changes: 12 additions & 0 deletions VPLink.Common/Configuration/IChatConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@ public interface IChatConfiguration
/// </summary>
/// <value>The font style.</value>
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; }
}
107 changes: 107 additions & 0 deletions VPLink.Common/Data/PlainTextMessageBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Cysharp.Text;
using Humanizer;

namespace VPLink.Common.Data;

/// <summary>
/// Represents a plain text message builder.
/// </summary>
public struct PlainTextMessageBuilder : IDisposable
{
private Utf8ValueStringBuilder _builder;

/// <summary>
/// Initializes a new instance of the <see cref="PlainTextMessageBuilder" /> struct.
/// </summary>
public PlainTextMessageBuilder()
{
_builder = ZString.CreateUtf8StringBuilder();
}

/// <summary>
/// Appends the specified word.
/// </summary>
/// <param name="word">The word.</param>
/// <param name="whitespace">The trailing whitespace trivia.</param>
public void AddWord(ReadOnlySpan<char> word, char whitespace = ' ')
{
_builder.Append(word);
if (whitespace != '\0') _builder.Append(whitespace);
}

/// <summary>
/// Appends the specified word.
/// </summary>
/// <param name="timestamp">The timestamp.</param>
/// <param name="format">The format.</param>
/// <param name="whitespace">The trailing whitespace trivia.</param>
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($"<t:{timestamp.ToUnixTimeSeconds():D}:{format}>", whitespace);
break;
}
}

/// <summary>
/// Clears the builder.
/// </summary>
public void Clear()
{
_builder.Clear();
}

/// <inheritdoc />
public void Dispose()
{
_builder.Dispose();
}

/// <inheritdoc />
public override string ToString()
{
return _builder.ToString().Trim();
}
}
16 changes: 12 additions & 4 deletions VPLink.Common/Data/RelayedMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,29 @@ public readonly struct RelayedMessage
/// </summary>
/// <param name="author">The author.</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;
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>
/// Gets the message content.
/// </summary>
/// <value>The message content.</value>
public string Content { get; }

/// <summary>
/// Gets the user that sent the message.
/// Gets a value indicating whether this message is a reply.
/// </summary>
/// <value>The user that sent the message.</value>
public string Author { get; }
/// <value><see langword="true" /> if this message is a reply; otherwise, <see langword="false" />.</value>
public bool IsReply { get; }
}
13 changes: 13 additions & 0 deletions VPLink.Common/Data/TimestampFormat.cs
Original file line number Diff line number Diff line change
@@ -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'
}
27 changes: 27 additions & 0 deletions VPLink.Common/Extensions/UserExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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)
{
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;
}
}
123 changes: 123 additions & 0 deletions VPLink.Common/MentionUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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<char> 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<char> 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<char> 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<char> 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<char> contents,
ref PlainTextMessageBuilder builder,
char whitespaceTrivia)
{
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)
{
case '\\':
isEscaped = !isEscaped;
break;

case ':' when !isEscaped && index + 1 < contents.Length:
formatSpecifier = contents[index + 1];
if (formatSpecifier == '>') formatSpecifier = '\0'; // ignore closing tag
breakLoop = true;
break;

case '>' when !isEscaped:
break;

case var _ when char.IsDigit(current):
buffer.Append(current);
break;

default:
return;
}
}

ReadOnlySpan<byte> bytes = buffer.AsSpan();
int charCount = Encoding.UTF8.GetCharCount(bytes);
Span<char> chars = stackalloc char[charCount];
Encoding.UTF8.GetChars(bytes, chars);

DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(chars, CultureInfo.InvariantCulture));
builder.AddTimestamp(timestamp, (TimestampFormat)formatSpecifier, whitespaceTrivia);
}
}
2 changes: 2 additions & 0 deletions VPLink.Common/VPLink.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="VpSharp" Version="0.1.0-nightly.43"/>
</ItemGroup>

Expand Down
57 changes: 57 additions & 0 deletions VPLink/Commands/InfoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Text;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using VPLink.Services;

namespace VPLink.Commands;

/// <summary>
/// Represents a class which implements the <c>info</c> command.
/// </summary>
internal sealed class InfoCommand : InteractionModuleBase<SocketInteractionContext>
{
private readonly BotService _botService;
private readonly DiscordSocketClient _discordClient;

/// <summary>
/// Initializes a new instance of the <see cref="InfoCommand" /> class.
/// </summary>
/// <param name="botService">The bot service.</param>
/// <param name="discordClient"></param>
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", $"<t:{_botService.StartedAt.ToUnixTimeSeconds()}:R>", 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);
}
}
Loading

0 comments on commit f4b5ff3

Please sign in to comment.