feat: announce avatar events

This commit is contained in:
Oliver Booth 2023-08-26 13:03:42 +01:00
parent 69edcfe3f5
commit 7a6ae083da
Signed by: oliverbooth
GPG Key ID: B89D139977693FED
5 changed files with 127 additions and 14 deletions

View File

@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Discord; using Discord;
using Discord.Interactions; using Discord.Interactions;
@ -9,7 +9,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using VPLink.Commands; using VPLink.Commands;
using VpSharp;
using VpSharp.Entities; using VpSharp.Entities;
namespace VPLink.Services; namespace VPLink.Services;
@ -25,7 +24,6 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly InteractionService _interactionService; private readonly InteractionService _interactionService;
private readonly DiscordSocketClient _discordClient; private readonly DiscordSocketClient _discordClient;
private readonly VirtualParadiseClient _virtualParadiseClient;
private readonly Subject<IUserMessage> _messageReceived = new(); private readonly Subject<IUserMessage> _messageReceived = new();
/// <summary> /// <summary>
@ -36,20 +34,17 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
/// <param name="configuration">The configuration.</param> /// <param name="configuration">The configuration.</param>
/// <param name="interactionService">The interaction service.</param> /// <param name="interactionService">The interaction service.</param>
/// <param name="discordClient">The Discord client.</param> /// <param name="discordClient">The Discord client.</param>
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
public DiscordService(ILogger<DiscordService> logger, public DiscordService(ILogger<DiscordService> logger,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IConfiguration configuration, IConfiguration configuration,
InteractionService interactionService, InteractionService interactionService,
DiscordSocketClient discordClient, DiscordSocketClient discordClient)
VirtualParadiseClient virtualParadiseClient)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_configuration = configuration; _configuration = configuration;
_interactionService = interactionService; _interactionService = interactionService;
_discordClient = discordClient; _discordClient = discordClient;
_virtualParadiseClient = virtualParadiseClient;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -114,6 +109,38 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
return _interactionService.RegisterCommandsGloballyAsync(); return _interactionService.RegisterCommandsGloballyAsync();
} }
/// <inheritdoc />
public Task AnnounceArrival(VirtualParadiseAvatar avatar)
{
if (avatar is null) throw new ArgumentNullException(nameof(avatar));
if (!TryGetRelayChannel(out ITextChannel? channel)) return Task.CompletedTask;
var embed = new EmbedBuilder();
embed.WithColor(0x00FF00);
embed.WithTitle("📥 Avatar Joined");
embed.WithDescription(avatar.Name);
embed.WithTimestamp(DateTimeOffset.UtcNow);
embed.WithFooter($"Session {avatar.Session}");
return channel.SendMessageAsync(embed: embed.Build());
}
/// <inheritdoc />
public Task AnnounceDeparture(VirtualParadiseAvatar avatar)
{
if (avatar is null) throw new ArgumentNullException(nameof(avatar));
if (!TryGetRelayChannel(out ITextChannel? channel)) return Task.CompletedTask;
var embed = new EmbedBuilder();
embed.WithColor(0xFF0000);
embed.WithTitle("📤 Avatar Left");
embed.WithDescription(avatar.Name);
embed.WithTimestamp(DateTimeOffset.UtcNow);
embed.WithFooter($"Session {avatar.Session}");
return channel.SendMessageAsync(embed: embed.Build());
}
/// <inheritdoc /> /// <inheritdoc />
public Task SendMessageAsync(VirtualParadiseMessage message) public Task SendMessageAsync(VirtualParadiseMessage message)
{ {
@ -134,12 +161,7 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
_logger.LogInformation("Message by {Author}: {Content}", author, message.Content); _logger.LogInformation("Message by {Author}: {Content}", author, message.Content);
var channelId = _configuration.GetSection("Discord:ChannelId").Get<ulong>(); if (!TryGetRelayChannel(out ITextChannel? channel)) return Task.CompletedTask;
if (_discordClient.GetChannel(channelId) is not ITextChannel channel)
{
_logger.LogError("Channel {ChannelId} does not exist", channelId);
return Task.CompletedTask;
}
string unescaped = UnescapeRegex.Replace(message.Content, "$1"); string unescaped = UnescapeRegex.Replace(message.Content, "$1");
string escaped = EscapeRegex.Replace(unescaped, "\\$1"); string escaped = EscapeRegex.Replace(unescaped, "\\$1");
@ -148,6 +170,20 @@ internal sealed partial class DiscordService : BackgroundService, IDiscordServic
return channel.SendMessageAsync($"**{displayName}**: {escaped}"); return channel.SendMessageAsync($"**{displayName}**: {escaped}");
} }
private bool TryGetRelayChannel([NotNullWhen(true)] out ITextChannel? channel)
{
var channelId = _configuration.GetValue<ulong>("Discord:ChannelId");
if (_discordClient.GetChannel(channelId) is ITextChannel textChannel)
{
channel = textChannel;
return true;
}
_logger.LogError("Channel {ChannelId} does not exist", channelId);
channel = null;
return false;
}
[GeneratedRegex(@"\\(\*|_|`|~|\\)", RegexOptions.Compiled)] [GeneratedRegex(@"\\(\*|_|`|~|\\)", RegexOptions.Compiled)]
private static partial Regex GetUnescapeRegex(); private static partial Regex GetUnescapeRegex();

View File

@ -14,10 +14,24 @@ public interface IDiscordService
/// <value>An observable that is triggered when a message is received from the Discord channel.</value> /// <value>An observable that is triggered when a message is received from the Discord channel.</value>
IObservable<IUserMessage> OnMessageReceived { get; } IObservable<IUserMessage> OnMessageReceived { get; }
/// <summary>
/// Announces the arrival of an avatar.
/// </summary>
/// <param name="avatar">The avatar.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
Task AnnounceArrival(VirtualParadiseAvatar avatar);
/// <summary>
/// Announces the arrival of an avatar.
/// </summary>
/// <param name="avatar">The avatar.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
Task AnnounceDeparture(VirtualParadiseAvatar avatar);
/// <summary> /// <summary>
/// Sends a message to the Discord channel. /// Sends a message to the Discord channel.
/// </summary> /// </summary>
/// <param name="message">The message to send.</param> /// <param name="message">The message to send.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
Task SendMessageAsync(VirtualParadiseMessage message); Task SendMessageAsync(VirtualParadiseMessage message);
} }

View File

@ -8,6 +8,22 @@ namespace VPLink.Services;
/// </summary> /// </summary>
public interface IVirtualParadiseService public interface IVirtualParadiseService
{ {
/// <summary>
/// Gets an observable that is triggered when an avatar enters the Virtual Paradise world.
/// </summary>
/// <value>
/// An observable that is triggered when an avatar enters the Virtual Paradise world.
/// </value>
IObservable<VirtualParadiseAvatar> OnAvatarJoined { get; }
/// <summary>
/// Gets an observable that is triggered when an avatar exits the Virtual Paradise world.
/// </summary>
/// <value>
/// An observable that is triggered when an avatar exits the Virtual Paradise world.
/// </value>
IObservable<VirtualParadiseAvatar> OnAvatarLeft { get; }
/// <summary> /// <summary>
/// Gets an observable that is triggered when a message is received from the Virtual Paradise world server. /// Gets an observable that is triggered when a message is received from the Virtual Paradise world server.
/// </summary> /// </summary>

View File

@ -45,6 +45,9 @@ internal sealed class RelayService : BackgroundService
.Where(m => m.Author != _discordClient.CurrentUser) .Where(m => m.Author != _discordClient.CurrentUser)
.SubscribeAsync(_virtualParadiseService.SendMessageAsync); .SubscribeAsync(_virtualParadiseService.SendMessageAsync);
_virtualParadiseService.OnAvatarJoined.SubscribeAsync(_discordService.AnnounceArrival);
_virtualParadiseService.OnAvatarLeft.SubscribeAsync(_discordService.AnnounceDeparture);
_virtualParadiseService.OnMessageReceived _virtualParadiseService.OnMessageReceived
.Where(m => m.Author != _virtualParadiseClient.CurrentAvatar) .Where(m => m.Author != _virtualParadiseClient.CurrentAvatar)
.SubscribeAsync(_discordService.SendMessageAsync); .SubscribeAsync(_discordService.SendMessageAsync);

View File

@ -17,6 +17,8 @@ internal sealed class VirtualParadiseService : BackgroundService, IVirtualParadi
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly VirtualParadiseClient _virtualParadiseClient; private readonly VirtualParadiseClient _virtualParadiseClient;
private readonly Subject<VirtualParadiseMessage> _messageReceived = new(); private readonly Subject<VirtualParadiseMessage> _messageReceived = new();
private readonly Subject<VirtualParadiseAvatar> _avatarJoined = new();
private readonly Subject<VirtualParadiseAvatar> _avatarLeft = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="VirtualParadiseService" /> class. /// Initializes a new instance of the <see cref="VirtualParadiseService" /> class.
@ -33,6 +35,12 @@ internal sealed class VirtualParadiseService : BackgroundService, IVirtualParadi
_virtualParadiseClient = virtualParadiseClient; _virtualParadiseClient = virtualParadiseClient;
} }
/// <inheritdoc />
public IObservable<VirtualParadiseAvatar> OnAvatarJoined => _avatarJoined.AsObservable();
/// <inheritdoc />
public IObservable<VirtualParadiseAvatar> OnAvatarLeft => _avatarJoined.AsObservable();
/// <inheritdoc /> /// <inheritdoc />
public IObservable<VirtualParadiseMessage> OnMessageReceived => _messageReceived.AsObservable(); public IObservable<VirtualParadiseMessage> OnMessageReceived => _messageReceived.AsObservable();
@ -59,6 +67,8 @@ internal sealed class VirtualParadiseService : BackgroundService, IVirtualParadi
{ {
_logger.LogInformation("Establishing relay"); _logger.LogInformation("Establishing relay");
_virtualParadiseClient.MessageReceived.Subscribe(_messageReceived); _virtualParadiseClient.MessageReceived.Subscribe(_messageReceived);
_virtualParadiseClient.AvatarJoined.Subscribe(OnVirtualParadiseAvatarJoined);
_virtualParadiseClient.AvatarLeft.Subscribe(OnVirtualParadiseAvatarLeft);
string username = _configuration.GetSection("VirtualParadise:Username").Value ?? string username = _configuration.GetSection("VirtualParadise:Username").Value ??
throw new InvalidOperationException("Username is not set."); throw new InvalidOperationException("Username is not set.");
@ -76,4 +86,38 @@ internal sealed class VirtualParadiseService : BackgroundService, IVirtualParadi
_logger.LogInformation("Entering world {World}", world); _logger.LogInformation("Entering world {World}", world);
await _virtualParadiseClient.EnterAsync(world).ConfigureAwait(false); await _virtualParadiseClient.EnterAsync(world).ConfigureAwait(false);
} }
private void OnVirtualParadiseAvatarJoined(VirtualParadiseAvatar avatar)
{
if (!_configuration.GetValue<bool>("Bot:AnnounceAvatarEvents"))
{
_logger.LogDebug("Join/leave events are disabled, ignoring event");
return;
}
if (avatar.IsBot && !_configuration.GetSection("Bot:AnnounceBots").Get<bool>())
{
_logger.LogDebug("Bot events are disabled, ignoring event");
return;
}
_avatarJoined.OnNext(avatar);
}
private void OnVirtualParadiseAvatarLeft(VirtualParadiseAvatar avatar)
{
if (!_configuration.GetValue<bool>("Bot:AnnounceAvatarEvents"))
{
_logger.LogDebug("Join/leave events are disabled, ignoring event");
return;
}
if (avatar.IsBot && !_configuration.GetSection("Bot:AnnounceBots").Get<bool>())
{
_logger.LogDebug("Bot events are disabled, ignoring event");
return;
}
_avatarLeft.OnNext(avatar);
}
} }