-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'release/1.3.0' into main
- Loading branch information
Showing
17 changed files
with
706 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.