diff --git a/VpBridge/Commands/WhoCommand.cs b/VpBridge/Commands/WhoCommand.cs
new file mode 100644
index 0000000..e1c4d1d
--- /dev/null
+++ b/VpBridge/Commands/WhoCommand.cs
@@ -0,0 +1,73 @@
+using Cysharp.Text;
+using Discord;
+using Discord.Interactions;
+using VpSharp;
+using VpSharp.Entities;
+
+namespace VpBridge.Commands;
+
+///
+/// Represents a class which implements the who command.
+///
+internal sealed class WhoCommand : InteractionModuleBase
+{
+ private readonly VirtualParadiseClient _virtualParadiseClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Virtual Paradise client.
+ public WhoCommand(VirtualParadiseClient virtualParadiseClient)
+ {
+ _virtualParadiseClient = virtualParadiseClient;
+ }
+
+ [SlashCommand("who", "Displays a list of active users in Virtual Paradise.")]
+ [RequireContext(ContextType.Guild)]
+ public async Task HandleAsync()
+ {
+ var embed = new EmbedBuilder();
+ embed.WithColor(0x1E88E5);
+ embed.WithAuthor($"🌎 {_virtualParadiseClient.CurrentWorld?.Name}");
+ embed.WithTitle("Active Users");
+ embed.WithTimestamp(DateTimeOffset.UtcNow);
+
+ using Utf8ValueStringBuilder userBuilder = ZString.CreateUtf8StringBuilder();
+ using Utf8ValueStringBuilder botsBuilder = ZString.CreateUtf8StringBuilder();
+ var userCount = 0;
+ var botCount = 0;
+
+ foreach (VirtualParadiseAvatar avatar in _virtualParadiseClient.Avatars)
+ {
+ if (avatar.IsBot)
+ {
+ botsBuilder.AppendLine($"* {avatar.Name} ({avatar.Session})");
+ botCount++;
+ }
+ else
+ {
+ userBuilder.AppendLine($"* {avatar.Name} ({avatar.Session})");
+ userCount++;
+ }
+ }
+
+ string userTitle = userCount switch
+ {
+ 0 => "Users",
+ 1 => "1 User",
+ _ => $"{userCount} Users"
+ };
+
+ string botTitle = botCount switch
+ {
+ 0 => "Bots",
+ 1 => "1 Bot",
+ _ => $"{botCount} Bots"
+ };
+
+ embed.AddField($"👤 {userTitle}", userCount > 0 ? userBuilder.ToString() : "*None*", true);
+ embed.AddField($"🤖 {botTitle}", botCount > 0 ? botsBuilder.ToString() : "*None*", true);
+
+ await RespondAsync(embed: embed.Build());
+ }
+}
diff --git a/VpBridge/Program.cs b/VpBridge/Program.cs
index 49e4679..b63efc9 100644
--- a/VpBridge/Program.cs
+++ b/VpBridge/Program.cs
@@ -1,4 +1,5 @@
-using Discord;
+using Discord;
+using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -24,10 +25,13 @@ builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.AddSingleton();
-builder.Services.AddSingleton(new DiscordSocketClient(new DiscordSocketConfig
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
-}));
+});
builder.Services.AddHostedSingleton();
builder.Services.AddHostedSingleton();
diff --git a/VpBridge/Services/DiscordService.cs b/VpBridge/Services/DiscordService.cs
index f75c9c0..52c8669 100644
--- a/VpBridge/Services/DiscordService.cs
+++ b/VpBridge/Services/DiscordService.cs
@@ -1,12 +1,14 @@
-using System.Reactive.Linq;
+using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Text.RegularExpressions;
using Discord;
+using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using VpBridge.Commands;
using VpSharp;
using VpSharp.Entities;
@@ -19,7 +21,9 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
private static readonly Regex EscapeRegex = GetEscapeRegex();
private readonly ILogger _logger;
+ private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _configuration;
+ private readonly InteractionService _interactionService;
private readonly DiscordSocketClient _discordClient;
private readonly VirtualParadiseClient _virtualParadiseClient;
private readonly Subject _messageReceived = new();
@@ -28,16 +32,22 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
/// Initializes a new instance of the class.
///
/// The logger.
+ /// The service provider.
/// The configuration.
+ /// The interaction service.
/// The Discord client.
/// The Virtual Paradise client.
public DiscordService(ILogger logger,
+ IServiceProvider serviceProvider,
IConfiguration configuration,
+ InteractionService interactionService,
DiscordSocketClient discordClient,
VirtualParadiseClient virtualParadiseClient)
{
_logger = logger;
+ _serviceProvider = serviceProvider;
_configuration = configuration;
+ _interactionService = interactionService;
_discordClient = discordClient;
_virtualParadiseClient = virtualParadiseClient;
}
@@ -49,40 +59,13 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Establishing relay");
- _discordClient.MessageReceived += arg =>
- {
- if (arg is not IUserMessage message)
- return Task.CompletedTask;
- if (message.Channel.Id != _configuration.GetSection("Discord:ChannelId").Get())
- return Task.CompletedTask;
+ _logger.LogInformation("Adding command modules");
+ await _interactionService.AddModuleAsync(_serviceProvider).ConfigureAwait(false);
- if (message.Content.Equals("!who"))
- {
- VirtualParadiseAvatar[] avatars = _virtualParadiseClient.Avatars.Where(a => !a.IsBot).ToArray();
- int count = avatars.Length;
-
- if (count > 0)
- {
- var builder = new StringBuilder();
- builder.AppendLine("**Users In World 🌎**");
- foreach (VirtualParadiseAvatar avatar in _virtualParadiseClient.Avatars)
- {
- if (avatar.IsBot || avatar == _virtualParadiseClient.CurrentAvatar)
- continue;
-
- builder.AppendLine($"• {avatar.Name}");
- }
-
- return message.ReplyAsync(builder.ToString());
- }
-
- return message.ReplyAsync("**No Users In World 🚫**");
- }
-
- _messageReceived.OnNext(message);
- return Task.CompletedTask;
- };
+ _discordClient.Ready += OnReady;
+ _discordClient.InteractionCreated += OnInteractionCreated;
+ _discordClient.MessageReceived += OnDiscordMessageReceived;
string token = _configuration.GetSection("Discord:Token").Value ??
throw new InvalidOperationException("Token is not set.");
@@ -92,6 +75,45 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
await _discordClient.StartAsync();
}
+ private Task OnDiscordMessageReceived(SocketMessage arg)
+ {
+ if (arg is not IUserMessage message)
+ return Task.CompletedTask;
+
+ if (message.Channel.Id != _configuration.GetSection("Discord:ChannelId").Get())
+ return Task.CompletedTask;
+
+ _messageReceived.OnNext(message);
+ return Task.CompletedTask;
+ }
+
+ private async Task OnInteractionCreated(SocketInteraction interaction)
+ {
+ try
+ {
+ var context = new SocketInteractionContext(_discordClient, interaction);
+ IResult result = await _interactionService.ExecuteCommandAsync(context, _serviceProvider);
+
+ if (!result.IsSuccess)
+ switch (result.Error)
+ {
+ case InteractionCommandError.UnmetPrecondition:
+ break;
+ }
+ }
+ catch
+ {
+ if (interaction.Type is InteractionType.ApplicationCommand)
+ await interaction.GetOriginalResponseAsync().ContinueWith(async msg => await msg.Result.DeleteAsync());
+ }
+ }
+
+ private Task OnReady()
+ {
+ _logger.LogInformation("Discord client ready");
+ return _interactionService.RegisterCommandsGloballyAsync();
+ }
+
///
public Task SendMessageAsync(VirtualParadiseMessage message)
{
diff --git a/VpBridge/VpBridge.csproj b/VpBridge/VpBridge.csproj
index e61747e..a302290 100644
--- a/VpBridge/VpBridge.csproj
+++ b/VpBridge/VpBridge.csproj
@@ -52,6 +52,7 @@
+