mirror of
https://github.com/oliverbooth/VPLink
synced 2024-11-09 23:25:42 +00:00
Merge branch 'release/1.2.0' into main
This commit is contained in:
commit
0e527b8e05
47
CONTRIBUTING.md
Normal file
47
CONTRIBUTING.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
## How you can contribute
|
||||||
|
|
||||||
|
Contributions to this project are always welcome. If you spot a bug, or want to request a new extension method, open a new issue
|
||||||
|
or submit a pull request.
|
||||||
|
|
||||||
|
### Pull request guidelines
|
||||||
|
|
||||||
|
This project uses C# 11.0 language features where feasible, and adheres to StyleCop rules with some minor adjustments.
|
||||||
|
There is an `.editorconfig` included in this repository. For quick and painless pull requests, ensure that the analyzer does not
|
||||||
|
throw warnings.
|
||||||
|
|
||||||
|
Please ensure that you follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
specification, as the GitHub release for this project is automatically generated from the commit history, and formatted using the
|
||||||
|
convetional commits specification.
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
|
||||||
|
Below are a few pointers to which you may refer, but keep in mind this is not an exhaustive list:
|
||||||
|
|
||||||
|
- Use C# 11.0 features where possible
|
||||||
|
- Try to ensure code is CLS-compliant. Where this is not possible, decorate methods with `CLSCompliantAttribute` and pass `false`
|
||||||
|
- Follow all .NET guidelines and coding conventions.
|
||||||
|
See https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
|
||||||
|
and https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
|
||||||
|
- Make full use of XMLDoc and be thorough - but concise - with all documentation
|
||||||
|
- Ensure that no line exceeds 130 characters in length
|
||||||
|
- Do NOT include file headers in any form
|
||||||
|
- Declare `using` directives outside of namespace scope
|
||||||
|
- Avoid using exceptions for flow control where possible
|
||||||
|
- Use braces, even for single-statement bodies
|
||||||
|
- Use implicit type when the type is apparent
|
||||||
|
- Use explicit type otherwise
|
||||||
|
- Use U.S. English throughout the codebase and documentation
|
||||||
|
|
||||||
|
When in doubt, follow .NET guidelines.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
When introducing a new extension method, you must ensure that you have also defined a unit test that asserts its correct behavior.
|
||||||
|
The code style guidelines and code-analysis rules apply to the `X10D.Tests` as much as `X10D`, although documentation may
|
||||||
|
be briefer. Refer to existing tests as a guideline.
|
||||||
|
|
||||||
|
### Disclaimer
|
||||||
|
|
||||||
|
In the event of a code style violation, a pull request may be left open (or closed entirely) without merging. Keep in mind this does
|
||||||
|
not mean the theory or implementation of the method is inherently bad or rejected entirely (although if this is the case, it will
|
||||||
|
be outlined)
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Oliver Booth
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
39
README.md
39
README.md
@ -1,3 +1,38 @@
|
|||||||
# VpBridge
|
<p align="center">
|
||||||
|
<img src="banner.png">
|
||||||
|
<a href="https://github.com/oliverbooth/VPLink/actions/workflows/dotnet.yml"><img src="https://img.shields.io/github/actions/workflow/status/oliverbooth/VPLink/dotnet.yml?style=flat-square" alt="GitHub Workflow Status" title="GitHub Workflow Status"></a>
|
||||||
|
<a href="https://github.com/oliverbooth/VPLink/issues"><img src="https://img.shields.io/github/issues/oliverbooth/VPLink?style=flat-square" alt="GitHub Issues" title="GitHub Issues"></a>
|
||||||
|
<a href="https://github.com/oliverbooth/VPLink/releases"><img alt="GitHub release" src="https://img.shields.io/github/v/release/oliverbooth/VPLink?style=flat-square"></a>
|
||||||
|
<a href="https://github.com/oliverbooth/VPLink/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/oliverbooth/VPLink?style=flat-square" alt="MIT License" title="MIT License"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
VpBridge is a simple bot for both Discord and Virtual Paradise which bridges chat messages from a designated Discord channel, to a world in Virtual Paradise.
|
### About
|
||||||
|
VPLink is a simple and customisable bot for both Discord and Virtual Paradise, which bridges chat messages between a
|
||||||
|
designated Discord channel, and the world where the bot is running. It is written in C# and uses the Discord.NET
|
||||||
|
library, as well as a [wrapper for the Virtual Paradise SDK](https://github.com/oliverbooth/VpSharp) that I wrote
|
||||||
|
myself.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) (or later)
|
||||||
|
- [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet/7.0) (or later)
|
||||||
|
- A [Virtual Paradise](https://www.virtualparadise.org/) user account
|
||||||
|
- A [Discord](https://discord.com/) user account, and a bot application
|
||||||
|
|
||||||
|
### Setup (docker-compose)
|
||||||
|
|
||||||
|
1. Clone the repository to your local machine.
|
||||||
|
2. Edit the `docker-compose.yml` file to your needs, including validating the mount paths.
|
||||||
|
3. /app/data (relative to the container) must contain a config.toml file with the fields populated, see the
|
||||||
|
[example config file](config.example.toml) for the available fields.
|
||||||
|
4. Run `docker-compose up -d` to start the bot.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
Please see the [contributing guidelines](CONTRIBUTING.md) for more information.
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
VPlink is licensed under the [MIT License](LICENSE.md). See the LICENSE.md file for more information.
|
33
VPLink.sln
Normal file
33
VPLink.sln
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VPLink", "VPLink\VPLink.csproj", "{CD488A1E-0232-4EB5-A381-38A42B267B11}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B1080228-7E6C-48CB-976B-21EF7F5DBC33}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
README.md = README.md
|
||||||
|
CONTRIBUTING.md = CONTRIBUTING.md
|
||||||
|
LICENSE.md = LICENSE.md
|
||||||
|
.github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md
|
||||||
|
.github\FUNDING.yml = .github\FUNDING.yml
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{BBE9420C-B35B-48C4-AF9B-53B0ED50E46B}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.github\workflows\docker-release.yml = .github\workflows\docker-release.yml
|
||||||
|
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
|
||||||
|
.github\workflows\prerelease.yml = .github\workflows\prerelease.yml
|
||||||
|
.github\workflows\release.yml = .github\workflows\release.yml
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
73
VPLink/Commands/WhoCommand.cs
Normal file
73
VPLink/Commands/WhoCommand.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
using Cysharp.Text;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using VpSharp;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
namespace VPLink.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a class which implements the <c>who</c> command.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class WhoCommand : InteractionModuleBase<SocketInteractionContext>
|
||||||
|
{
|
||||||
|
private readonly VirtualParadiseClient _virtualParadiseClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WhoCommand" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
33
VPLink/Configuration/BotConfiguration.cs
Normal file
33
VPLink/Configuration/BotConfiguration.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
namespace VPLink.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the bot configuration.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BotConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the bot should announce avatar events.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <see langword="true" /> if the bot should announce avatar events; otherwise, <see langword="false" />.
|
||||||
|
/// </value>
|
||||||
|
public bool AnnounceAvatarEvents { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the bot should announce avatar events for bots.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <see langword="true" /> if the bot should announce avatar events for bots; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </value>
|
||||||
|
public bool AnnounceBots { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the bot should relay messages from other bots.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// <see langword="true" /> if the bot should relay messages from other bots; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </value>
|
||||||
|
public bool RelayBotMessages { get; set; } = false;
|
||||||
|
}
|
21
VPLink/Configuration/ChatConfiguration.cs
Normal file
21
VPLink/Configuration/ChatConfiguration.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using VpSharp;
|
||||||
|
|
||||||
|
namespace VPLink.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the chat configuration.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChatConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the color of the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The message color.</value>
|
||||||
|
public uint Color { get; set; } = 0x191970;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the font style of the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The font style.</value>
|
||||||
|
public FontStyle Style { get; set; } = FontStyle.Regular;
|
||||||
|
}
|
19
VPLink/Configuration/DiscordConfiguration.cs
Normal file
19
VPLink/Configuration/DiscordConfiguration.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace VPLink.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the Discord configuration.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DiscordConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the channel ID to which the bot should relay messages.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The channel ID.</value>
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Discord token.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Discord token.</value>
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
}
|
37
VPLink/Configuration/VirtualParadiseConfiguration.cs
Normal file
37
VPLink/Configuration/VirtualParadiseConfiguration.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace VPLink.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the Virtual Paradise configuration.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class VirtualParadiseConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display name of the bot.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The display name.</value>
|
||||||
|
public string BotName { get; set; } = "VPLink";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the chat configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The chat configuration.</value>
|
||||||
|
public ChatConfiguration Chat { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the password with which to log in to Virtual Paradise.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The login password.</value>
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the username with which to log in to Virtual Paradise.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The login username.</value>
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the world into which the bot should enter.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The world to enter.</value>
|
||||||
|
public string World { get; set; } = string.Empty;
|
||||||
|
}
|
30
VPLink/Data/RelayedMessage.cs
Normal file
30
VPLink/Data/RelayedMessage.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
namespace VPLink.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a message that is relayed between Discord and Virtual Paradise.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct RelayedMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RelayedMessage" /> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="author">The author.</param>
|
||||||
|
/// <param name="content">The content.</param>
|
||||||
|
public RelayedMessage(string author, string content)
|
||||||
|
{
|
||||||
|
Author = author;
|
||||||
|
Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message content.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The message content.</value>
|
||||||
|
public string Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user that sent the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The user that sent the message.</value>
|
||||||
|
public string Author { get; }
|
||||||
|
}
|
20
VPLink/Dockerfile
Normal file
20
VPLink/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["VPLink/VPLink.csproj", "VpBridge/"]
|
||||||
|
RUN dotnet restore "VPLink/VPLink.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/VpBridge"
|
||||||
|
RUN dotnet build "VPLink.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "VPLink.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "VPLink.dll"]
|
@ -1,11 +1,12 @@
|
|||||||
using Discord;
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Tomlyn.Extensions.Configuration;
|
using Tomlyn.Extensions.Configuration;
|
||||||
using VpBridge.Services;
|
using VPLink.Services;
|
||||||
using VpSharp;
|
using VpSharp;
|
||||||
using X10D.Hosting.DependencyInjection;
|
using X10D.Hosting.DependencyInjection;
|
||||||
|
|
||||||
@ -24,13 +25,21 @@ builder.Logging.ClearProviders();
|
|||||||
builder.Logging.AddSerilog();
|
builder.Logging.AddSerilog();
|
||||||
|
|
||||||
builder.Services.AddSingleton<VirtualParadiseClient>();
|
builder.Services.AddSingleton<VirtualParadiseClient>();
|
||||||
builder.Services.AddSingleton(new DiscordSocketClient(new DiscordSocketConfig
|
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<InteractionService>();
|
||||||
|
builder.Services.AddSingleton<DiscordSocketClient>();
|
||||||
|
builder.Services.AddSingleton(new DiscordSocketConfig
|
||||||
{
|
{
|
||||||
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
|
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
|
||||||
}));
|
});
|
||||||
|
|
||||||
builder.Services.AddHostedSingleton<IVirtualParadiseService, VirtualParadiseService>();
|
builder.Services.AddHostedSingleton<IAvatarService, AvatarService>();
|
||||||
builder.Services.AddHostedSingleton<IDiscordService, DiscordService>();
|
builder.Services.AddHostedSingleton<IDiscordMessageService, DiscordMessageService>();
|
||||||
|
builder.Services.AddHostedSingleton<IVirtualParadiseMessageService, VirtualParadiseMessageService>();
|
||||||
|
|
||||||
|
builder.Services.AddHostedSingleton<DiscordService>();
|
||||||
|
builder.Services.AddHostedSingleton<VirtualParadiseService>();
|
||||||
builder.Services.AddHostedSingleton<RelayService>();
|
builder.Services.AddHostedSingleton<RelayService>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
70
VPLink/Services/AvatarService.cs
Normal file
70
VPLink/Services/AvatarService.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VPLink.Configuration;
|
||||||
|
using VpSharp;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IAvatarService" />
|
||||||
|
internal sealed class AvatarService : BackgroundService, IAvatarService
|
||||||
|
{
|
||||||
|
private readonly ILogger<AvatarService> _logger;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly VirtualParadiseClient _virtualParadiseClient;
|
||||||
|
private readonly Subject<VirtualParadiseAvatar> _avatarJoined = new();
|
||||||
|
private readonly Subject<VirtualParadiseAvatar> _avatarLeft = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AvatarService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
|
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
||||||
|
public AvatarService(ILogger<AvatarService> logger,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
VirtualParadiseClient virtualParadiseClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_virtualParadiseClient = virtualParadiseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<VirtualParadiseAvatar> OnAvatarJoined => _avatarJoined.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<VirtualParadiseAvatar> OnAvatarLeft => _avatarLeft.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_virtualParadiseClient.AvatarJoined.Subscribe(OnVPAvatarJoined);
|
||||||
|
_virtualParadiseClient.AvatarLeft.Subscribe(OnVPAvatarLeft);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVPAvatarJoined(VirtualParadiseAvatar avatar)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Avatar} joined", avatar);
|
||||||
|
|
||||||
|
BotConfiguration configuration = _configurationService.BotConfiguration;
|
||||||
|
if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_avatarJoined.OnNext(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVPAvatarLeft(VirtualParadiseAvatar avatar)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Avatar} left", avatar);
|
||||||
|
|
||||||
|
BotConfiguration configuration = _configurationService.BotConfiguration;
|
||||||
|
if (!configuration.AnnounceAvatarEvents || avatar.IsBot && !configuration.AnnounceBots)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_avatarLeft.OnNext(avatar);
|
||||||
|
}
|
||||||
|
}
|
37
VPLink/Services/ConfigurationService.cs
Normal file
37
VPLink/Services/ConfigurationService.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using VPLink.Configuration;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IConfigurationService" />
|
||||||
|
internal sealed class ConfigurationService : IConfigurationService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ConfigurationService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration"></param>
|
||||||
|
public ConfigurationService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public BotConfiguration BotConfiguration
|
||||||
|
{
|
||||||
|
get => _configuration.GetSection("Bot").Get<BotConfiguration>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DiscordConfiguration DiscordConfiguration
|
||||||
|
{
|
||||||
|
get => _configuration.GetSection("Discord").Get<DiscordConfiguration>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public VirtualParadiseConfiguration VirtualParadiseConfiguration
|
||||||
|
{
|
||||||
|
get => _configuration.GetSection("VirtualParadise").Get<VirtualParadiseConfiguration>()!;
|
||||||
|
}
|
||||||
|
}
|
163
VPLink/Services/DiscordMessageService.cs
Normal file
163
VPLink/Services/DiscordMessageService.cs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Cysharp.Text;
|
||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VPLink.Configuration;
|
||||||
|
using VPLink.Data;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IDiscordMessageService" />
|
||||||
|
internal sealed partial class DiscordMessageService : BackgroundService, IDiscordMessageService
|
||||||
|
{
|
||||||
|
private static readonly Encoding Utf8Encoding = new UTF8Encoding(false, false);
|
||||||
|
private static readonly Regex UnescapeRegex = GetUnescapeRegex();
|
||||||
|
private static readonly Regex EscapeRegex = GetEscapeRegex();
|
||||||
|
|
||||||
|
private readonly ILogger<DiscordMessageService> _logger;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly DiscordSocketClient _discordClient;
|
||||||
|
private readonly Subject<RelayedMessage> _messageReceived = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DiscordMessageService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
|
/// <param name="discordClient">The Discord client.</param>
|
||||||
|
public DiscordMessageService(ILogger<DiscordMessageService> logger,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
DiscordSocketClient discordClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_discordClient = discordClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<RelayedMessage> OnMessageReceived => _messageReceived.AsObservable();
|
||||||
|
|
||||||
|
/// <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 />
|
||||||
|
public Task SendMessageAsync(RelayedMessage message)
|
||||||
|
{
|
||||||
|
if (!TryGetRelayChannel(out ITextChannel? channel)) return Task.CompletedTask;
|
||||||
|
return channel.SendMessageAsync($"**{message.Author}**: {message.Content}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_discordClient.MessageReceived += OnDiscordMessageReceived;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnDiscordMessageReceived(SocketMessage arg)
|
||||||
|
{
|
||||||
|
if (arg is not IUserMessage message)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
IUser author = message.Author;
|
||||||
|
if (author.Id == _discordClient.CurrentUser.Id)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (author.IsBot && !_configurationService.BotConfiguration.RelayBotMessages)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
string displayName = author.GlobalName ?? author.Username;
|
||||||
|
string unescaped = UnescapeRegex.Replace(message.Content, "$1");
|
||||||
|
string content = EscapeRegex.Replace(unescaped, "\\$1");
|
||||||
|
|
||||||
|
IReadOnlyCollection<IAttachment> attachments = message.Attachments;
|
||||||
|
if (attachments.Count > 0)
|
||||||
|
{
|
||||||
|
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
|
||||||
|
for (var index = 0; index < attachments.Count; index++)
|
||||||
|
{
|
||||||
|
builder.AppendLine(attachments.ElementAt(index).Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// += allocates more than necessary, just interpolate
|
||||||
|
content = $"{content}\n{builder}";
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.Trim();
|
||||||
|
_logger.LogInformation("Message by {Author}: {Content}", author, content);
|
||||||
|
|
||||||
|
Span<byte> buffer = stackalloc byte[255]; // VP message length limit
|
||||||
|
var messages = new List<RelayedMessage>();
|
||||||
|
int byteCount = Utf8Encoding.GetByteCount(content);
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
while (offset < byteCount)
|
||||||
|
{
|
||||||
|
int length = Math.Min(byteCount - offset, 255);
|
||||||
|
Utf8Encoding.GetBytes(content.AsSpan(offset, length), buffer);
|
||||||
|
messages.Add(new RelayedMessage(displayName, Utf8Encoding.GetString(buffer)));
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.ForEach(_messageReceived.OnNext);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetRelayChannel([NotNullWhen(true)] out ITextChannel? channel)
|
||||||
|
{
|
||||||
|
DiscordConfiguration configuration = _configurationService.DiscordConfiguration;
|
||||||
|
ulong channelId = configuration.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)]
|
||||||
|
private static partial Regex GetUnescapeRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(\*|_|`|~|\\)", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex GetEscapeRegex();
|
||||||
|
}
|
85
VPLink/Services/DiscordService.cs
Normal file
85
VPLink/Services/DiscordService.cs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VPLink.Commands;
|
||||||
|
using VPLink.Configuration;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
internal sealed class DiscordService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscordService> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly InteractionService _interactionService;
|
||||||
|
private readonly DiscordSocketClient _discordClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DiscordService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="serviceProvider">The service provider.</param>
|
||||||
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
|
/// <param name="interactionService">The interaction service.</param>
|
||||||
|
/// <param name="discordClient">The Discord client.</param>
|
||||||
|
public DiscordService(ILogger<DiscordService> logger,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
InteractionService interactionService,
|
||||||
|
DiscordSocketClient discordClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_interactionService = interactionService;
|
||||||
|
_discordClient = discordClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Establishing relay");
|
||||||
|
|
||||||
|
_logger.LogInformation("Adding command modules");
|
||||||
|
await _interactionService.AddModuleAsync<WhoCommand>(_serviceProvider).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_discordClient.Ready += OnReady;
|
||||||
|
_discordClient.InteractionCreated += OnInteractionCreated;
|
||||||
|
|
||||||
|
DiscordConfiguration configuration = _configurationService.DiscordConfiguration;
|
||||||
|
string token = configuration.Token ?? throw new InvalidOperationException("Token is not set.");
|
||||||
|
|
||||||
|
_logger.LogDebug("Connecting to Discord");
|
||||||
|
await _discordClient.LoginAsync(TokenType.Bot, token);
|
||||||
|
await _discordClient.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
25
VPLink/Services/IAvatarService.cs
Normal file
25
VPLink/Services/IAvatarService.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service that listens for, and triggers, avatar events.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAvatarService
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
27
VPLink/Services/IConfigurationService.cs
Normal file
27
VPLink/Services/IConfigurationService.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using VPLink.Configuration;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration service.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigurationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the bot configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The bot configuration.</value>
|
||||||
|
BotConfiguration BotConfiguration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Discord configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Discord configuration.</value>
|
||||||
|
DiscordConfiguration DiscordConfiguration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Virtual Paradise configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The Virtual Paradise configuration.</value>
|
||||||
|
VirtualParadiseConfiguration VirtualParadiseConfiguration { get; }
|
||||||
|
}
|
32
VPLink/Services/IDiscordMessageService.cs
Normal file
32
VPLink/Services/IDiscordMessageService.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using VPLink.Data;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service that listens for messages from the Discord bridge channel.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDiscordMessageService : IRelayTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an observable that is triggered when a valid message is received from the Discord bridge channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// An observable that is triggered when a valid message is received from the Discord bridge channel.
|
||||||
|
/// </value>
|
||||||
|
IObservable<RelayedMessage> 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);
|
||||||
|
}
|
16
VPLink/Services/IRelayTarget.cs
Normal file
16
VPLink/Services/IRelayTarget.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using VPLink.Data;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an object that can be used as a relay target.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRelayTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a message to the relay target.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to send.</param>
|
||||||
|
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
|
||||||
|
Task SendMessageAsync(RelayedMessage message);
|
||||||
|
}
|
17
VPLink/Services/IVirtualParadiseMessageService.cs
Normal file
17
VPLink/Services/IVirtualParadiseMessageService.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using VPLink.Data;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service that listens for messages from the Virtual Paradise world.
|
||||||
|
/// </summary>
|
||||||
|
public interface IVirtualParadiseMessageService : IRelayTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an observable that is triggered when a valid message is received from the Virtual Paradise world.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// An observable that is triggered when a valid message is received from the Virtual Paradise world.
|
||||||
|
/// </value>
|
||||||
|
IObservable<RelayedMessage> OnMessageReceived { get; }
|
||||||
|
}
|
44
VPLink/Services/RelayService.cs
Normal file
44
VPLink/Services/RelayService.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VpSharp.Extensions;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
internal sealed class RelayService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<RelayService> _logger;
|
||||||
|
private readonly IAvatarService _avatarService;
|
||||||
|
private readonly IDiscordMessageService _discordService;
|
||||||
|
private readonly IVirtualParadiseMessageService _virtualParadiseService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RelayService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="discordService">The Discord service.</param>
|
||||||
|
/// <param name="avatarService">The avatar service.</param>
|
||||||
|
/// <param name="virtualParadiseService">The Virtual Paradise service.</param>
|
||||||
|
public RelayService(ILogger<RelayService> logger,
|
||||||
|
IAvatarService avatarService,
|
||||||
|
IDiscordMessageService discordService,
|
||||||
|
IVirtualParadiseMessageService virtualParadiseService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_discordService = discordService;
|
||||||
|
_avatarService = avatarService;
|
||||||
|
_virtualParadiseService = virtualParadiseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Establishing relay");
|
||||||
|
|
||||||
|
_avatarService.OnAvatarJoined.SubscribeAsync(_discordService.AnnounceArrival);
|
||||||
|
_avatarService.OnAvatarLeft.SubscribeAsync(_discordService.AnnounceDeparture);
|
||||||
|
_discordService.OnMessageReceived.SubscribeAsync(_virtualParadiseService.SendMessageAsync);
|
||||||
|
_virtualParadiseService.OnMessageReceived.SubscribeAsync(_discordService.SendMessageAsync);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
69
VPLink/Services/VirtualParadiseMessageService.cs
Normal file
69
VPLink/Services/VirtualParadiseMessageService.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VPLink.Configuration;
|
||||||
|
using VPLink.Data;
|
||||||
|
using VpSharp;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
using FontStyle = VpSharp.FontStyle;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="VPLink.Services.IVirtualParadiseMessageService" />
|
||||||
|
internal sealed class VirtualParadiseMessageService : BackgroundService, IVirtualParadiseMessageService
|
||||||
|
{
|
||||||
|
private readonly ILogger<VirtualParadiseMessageService> _logger;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly VirtualParadiseClient _virtualParadiseClient;
|
||||||
|
private readonly Subject<RelayedMessage> _messageReceived = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="VirtualParadiseMessageService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
|
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
||||||
|
public VirtualParadiseMessageService(ILogger<VirtualParadiseMessageService> logger,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
VirtualParadiseClient virtualParadiseClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_virtualParadiseClient = virtualParadiseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<RelayedMessage> OnMessageReceived => _messageReceived.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SendMessageAsync(RelayedMessage message)
|
||||||
|
{
|
||||||
|
ChatConfiguration configuration = _configurationService.VirtualParadiseConfiguration.Chat;
|
||||||
|
|
||||||
|
Color color = Color.FromArgb((int)configuration.Color);
|
||||||
|
FontStyle style = configuration.Style;
|
||||||
|
return _virtualParadiseClient.SendMessageAsync(message.Author, message.Content, style, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_virtualParadiseClient.MessageReceived.Subscribe(OnVPMessageReceived);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnVPMessageReceived(VirtualParadiseMessage message)
|
||||||
|
{
|
||||||
|
if (message is null) throw new ArgumentNullException(nameof(message));
|
||||||
|
if (message.Type != MessageType.ChatMessage) return;
|
||||||
|
if (message.Author == _virtualParadiseClient.CurrentAvatar) return;
|
||||||
|
if (message.Author.IsBot && !_configurationService.BotConfiguration.RelayBotMessages) return;
|
||||||
|
|
||||||
|
_logger.LogInformation("Message by {Author}: {Content}", message.Author, message.Content);
|
||||||
|
|
||||||
|
var relayedMessage = new RelayedMessage(message.Author.Name, message.Content);
|
||||||
|
_messageReceived.OnNext(relayedMessage);
|
||||||
|
}
|
||||||
|
}
|
52
VPLink/Services/VirtualParadiseService.cs
Normal file
52
VPLink/Services/VirtualParadiseService.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System.Reactive.Subjects;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VpSharp;
|
||||||
|
using VpSharp.Entities;
|
||||||
|
using VirtualParadiseConfiguration = VPLink.Configuration.VirtualParadiseConfiguration;
|
||||||
|
|
||||||
|
namespace VPLink.Services;
|
||||||
|
|
||||||
|
internal sealed class VirtualParadiseService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<VirtualParadiseService> _logger;
|
||||||
|
private readonly IConfigurationService _configurationService;
|
||||||
|
private readonly VirtualParadiseClient _virtualParadiseClient;
|
||||||
|
private readonly Subject<VirtualParadiseMessage> _messageReceived = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="VirtualParadiseService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="configurationService">The configuration service.</param>
|
||||||
|
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
||||||
|
public VirtualParadiseService(ILogger<VirtualParadiseService> logger,
|
||||||
|
IConfigurationService configurationService,
|
||||||
|
VirtualParadiseClient virtualParadiseClient)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_virtualParadiseClient = virtualParadiseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Establishing relay");
|
||||||
|
_virtualParadiseClient.MessageReceived.Subscribe(_messageReceived);
|
||||||
|
|
||||||
|
VirtualParadiseConfiguration configuration = _configurationService.VirtualParadiseConfiguration;
|
||||||
|
|
||||||
|
string username = configuration.Username ?? throw new InvalidOperationException("Username is not set.");
|
||||||
|
string password = configuration.Password ?? throw new InvalidOperationException("Password is not set.");
|
||||||
|
string world = configuration.World ?? throw new InvalidOperationException("World is not set.");
|
||||||
|
string botName = configuration.BotName ?? throw new InvalidOperationException("Bot name is not set.");
|
||||||
|
|
||||||
|
_logger.LogDebug("Connecting to Virtual Paradise");
|
||||||
|
await _virtualParadiseClient.ConnectAsync().ConfigureAwait(false);
|
||||||
|
await _virtualParadiseClient.LoginAsync(username, password, botName).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogInformation("Entering world {World}", world);
|
||||||
|
await _virtualParadiseClient.EnterAsync(world).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
<Authors>Oliver Booth</Authors>
|
<Authors>Oliver Booth</Authors>
|
||||||
<RepositoryUrl>https://github.com/oliverbooth/VpBridge</RepositoryUrl>
|
<RepositoryUrl>https://github.com/oliverbooth/VpBridge</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<VersionPrefix>1.1.0</VersionPrefix>
|
<VersionPrefix>1.2.0</VersionPrefix>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
|
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
|
||||||
@ -52,6 +52,7 @@
|
|||||||
<PackageReference Include="VpSharp" Version="0.1.0-nightly.43"/>
|
<PackageReference Include="VpSharp" Version="0.1.0-nightly.43"/>
|
||||||
<PackageReference Include="X10D" Version="3.3.1"/>
|
<PackageReference Include="X10D" Version="3.3.1"/>
|
||||||
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
|
<PackageReference Include="X10D.Hosting" Version="3.3.1"/>
|
||||||
|
<PackageReference Include="ZString" Version="2.5.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
16
VpBridge.sln
16
VpBridge.sln
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VpBridge", "VpBridge\VpBridge.csproj", "{CD488A1E-0232-4EB5-A381-38A42B267B11}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{CD488A1E-0232-4EB5-A381-38A42B267B11}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
@ -1,20 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 443
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["VpBridge/VpBridge.csproj", "VpBridge/"]
|
|
||||||
RUN dotnet restore "VpBridge/VpBridge.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/VpBridge"
|
|
||||||
RUN dotnet build "VpBridge.csproj" -c Release -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
RUN dotnet publish "VpBridge.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "VpBridge.dll"]
|
|
@ -1,134 +0,0 @@
|
|||||||
using System.Reactive.Linq;
|
|
||||||
using System.Reactive.Subjects;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Discord;
|
|
||||||
using Discord.WebSocket;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using VpSharp;
|
|
||||||
using VpSharp.Entities;
|
|
||||||
|
|
||||||
namespace VpBridge.Services;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IDiscordService" />
|
|
||||||
internal sealed partial class DiscordService : BackgroundService, IDiscordService
|
|
||||||
{
|
|
||||||
private static readonly Regex UnescapeRegex = GetUnescapeRegex();
|
|
||||||
private static readonly Regex EscapeRegex = GetEscapeRegex();
|
|
||||||
|
|
||||||
private readonly ILogger<DiscordService> _logger;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly DiscordSocketClient _discordClient;
|
|
||||||
private readonly VirtualParadiseClient _virtualParadiseClient;
|
|
||||||
private readonly Subject<IUserMessage> _messageReceived = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DiscordService" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="configuration">The configuration.</param>
|
|
||||||
/// <param name="discordClient">The Discord client.</param>
|
|
||||||
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
|
||||||
public DiscordService(ILogger<DiscordService> logger,
|
|
||||||
IConfiguration configuration,
|
|
||||||
DiscordSocketClient discordClient,
|
|
||||||
VirtualParadiseClient virtualParadiseClient)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
_discordClient = discordClient;
|
|
||||||
_virtualParadiseClient = virtualParadiseClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IObservable<IUserMessage> OnMessageReceived => _messageReceived.AsObservable();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<ulong>())
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
string token = _configuration.GetSection("Discord:Token").Value ??
|
|
||||||
throw new InvalidOperationException("Token is not set.");
|
|
||||||
|
|
||||||
_logger.LogDebug("Connecting to Discord");
|
|
||||||
await _discordClient.LoginAsync(TokenType.Bot, token);
|
|
||||||
await _discordClient.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task SendMessageAsync(VirtualParadiseMessage message)
|
|
||||||
{
|
|
||||||
if (message is null) throw new ArgumentNullException(nameof(message));
|
|
||||||
if (string.IsNullOrWhiteSpace(message.Content)) return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (message.Author is not { } author)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Received message without author, ignoring message");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (author.IsBot && !_configuration.GetSection("Bot:RelayBotMessages").Get<bool>())
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Bot messages are disabled, ignoring message");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Message by {Author}: {Content}", author, message.Content);
|
|
||||||
|
|
||||||
var channelId = _configuration.GetSection("Discord:ChannelId").Get<ulong>();
|
|
||||||
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 escaped = EscapeRegex.Replace(unescaped, "\\$1");
|
|
||||||
|
|
||||||
string displayName = author.Name;
|
|
||||||
return channel.SendMessageAsync($"**{displayName}**: {escaped}");
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex(@"\\(\*|_|`|~|\\)", RegexOptions.Compiled)]
|
|
||||||
private static partial Regex GetUnescapeRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex(@"(\*|_|`|~|\\)", RegexOptions.Compiled)]
|
|
||||||
private static partial Regex GetEscapeRegex();
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using Discord;
|
|
||||||
using VpSharp.Entities;
|
|
||||||
|
|
||||||
namespace VpBridge.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a service that sends messages to the Discord channel.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDiscordService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an observable that is triggered when a message is received from the Discord channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>An observable that is triggered when a message is received from the Discord channel.</value>
|
|
||||||
IObservable<IUserMessage> OnMessageReceived { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a message to the Discord channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to send.</param>
|
|
||||||
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
|
|
||||||
Task SendMessageAsync(VirtualParadiseMessage message);
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
using Discord;
|
|
||||||
using VpSharp.Entities;
|
|
||||||
|
|
||||||
namespace VpBridge.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a service that sends messages to the Virtual Paradise world server.
|
|
||||||
/// </summary>
|
|
||||||
public interface IVirtualParadiseService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an observable that is triggered when a message is received from the Virtual Paradise world server.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>
|
|
||||||
/// An observable that is triggered when a message is received from the Virtual Paradise world server.
|
|
||||||
/// </value>
|
|
||||||
IObservable<VirtualParadiseMessage> OnMessageReceived { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a message to the Virtual Paradise world server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The Discord message to send.</param>
|
|
||||||
/// <returns>A <see cref="Task" /> representing the asynchronous operation.</returns>
|
|
||||||
Task SendMessageAsync(IUserMessage message);
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
using System.Reactive.Linq;
|
|
||||||
using Discord.WebSocket;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using VpSharp;
|
|
||||||
using VpSharp.Extensions;
|
|
||||||
|
|
||||||
namespace VpBridge.Services;
|
|
||||||
|
|
||||||
internal sealed class RelayService : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly ILogger<RelayService> _logger;
|
|
||||||
private readonly IDiscordService _discordService;
|
|
||||||
private readonly IVirtualParadiseService _virtualParadiseService;
|
|
||||||
private readonly DiscordSocketClient _discordClient;
|
|
||||||
private readonly VirtualParadiseClient _virtualParadiseClient;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RelayService" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="discordService">The Discord service.</param>
|
|
||||||
/// <param name="virtualParadiseService">The Virtual Paradise service.</param>
|
|
||||||
/// <param name="discordClient">The Discord client.</param>
|
|
||||||
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
|
||||||
public RelayService(ILogger<RelayService> logger,
|
|
||||||
IDiscordService discordService,
|
|
||||||
IVirtualParadiseService virtualParadiseService,
|
|
||||||
DiscordSocketClient discordClient,
|
|
||||||
VirtualParadiseClient virtualParadiseClient)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_discordService = discordService;
|
|
||||||
_virtualParadiseService = virtualParadiseService;
|
|
||||||
_discordClient = discordClient;
|
|
||||||
_virtualParadiseClient = virtualParadiseClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Establishing relay");
|
|
||||||
|
|
||||||
_discordService.OnMessageReceived
|
|
||||||
.Where(m => m.Author != _discordClient.CurrentUser)
|
|
||||||
.SubscribeAsync(_virtualParadiseService.SendMessageAsync);
|
|
||||||
|
|
||||||
_virtualParadiseService.OnMessageReceived
|
|
||||||
.Where(m => m.Author != _virtualParadiseClient.CurrentAvatar)
|
|
||||||
.SubscribeAsync(_discordService.SendMessageAsync);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
using System.Reactive.Linq;
|
|
||||||
using System.Reactive.Subjects;
|
|
||||||
using Discord;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using VpSharp;
|
|
||||||
using VpSharp.Entities;
|
|
||||||
using Color = System.Drawing.Color;
|
|
||||||
|
|
||||||
namespace VpBridge.Services;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IVirtualParadiseService" />
|
|
||||||
internal sealed class VirtualParadiseService : BackgroundService, IVirtualParadiseService
|
|
||||||
{
|
|
||||||
private readonly ILogger<VirtualParadiseService> _logger;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly VirtualParadiseClient _virtualParadiseClient;
|
|
||||||
private readonly Subject<VirtualParadiseMessage> _messageReceived = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VirtualParadiseService" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="configuration">The configuration.</param>
|
|
||||||
/// <param name="virtualParadiseClient">The Virtual Paradise client.</param>
|
|
||||||
public VirtualParadiseService(ILogger<VirtualParadiseService> logger,
|
|
||||||
IConfiguration configuration,
|
|
||||||
VirtualParadiseClient virtualParadiseClient)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
_virtualParadiseClient = virtualParadiseClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IObservable<VirtualParadiseMessage> OnMessageReceived => _messageReceived.AsObservable();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task SendMessageAsync(IUserMessage message)
|
|
||||||
{
|
|
||||||
if (message is null) throw new ArgumentNullException(nameof(message));
|
|
||||||
if (string.IsNullOrWhiteSpace(message.Content)) return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (message.Author.IsBot && !_configuration.GetSection("Bot:RelayBotMessages").Get<bool>())
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Bot messages are disabled, ignoring message");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Message by {Author}: {Content}", message.Author, message.Content);
|
|
||||||
|
|
||||||
string displayName = message.Author.GlobalName ?? message.Author.Username;
|
|
||||||
return _virtualParadiseClient.SendMessageAsync(displayName, message.Content, FontStyle.Bold, Color.MidnightBlue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Establishing relay");
|
|
||||||
_virtualParadiseClient.MessageReceived.Subscribe(_messageReceived);
|
|
||||||
|
|
||||||
string username = _configuration.GetSection("VirtualParadise:Username").Value ??
|
|
||||||
throw new InvalidOperationException("Username is not set.");
|
|
||||||
string password = _configuration.GetSection("VirtualParadise:Password").Value ??
|
|
||||||
throw new InvalidOperationException("Password is not set.");
|
|
||||||
string world = _configuration.GetSection("VirtualParadise:World").Value ??
|
|
||||||
throw new InvalidOperationException("World is not set.");
|
|
||||||
string botName = _configuration.GetSection("VirtualParadise:BotName").Value ??
|
|
||||||
throw new InvalidOperationException("Bot name is not set.");
|
|
||||||
|
|
||||||
_logger.LogDebug("Connecting to Virtual Paradise");
|
|
||||||
await _virtualParadiseClient.ConnectAsync().ConfigureAwait(false);
|
|
||||||
await _virtualParadiseClient.LoginAsync(username, password, botName).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_logger.LogInformation("Entering world {World}", world);
|
|
||||||
await _virtualParadiseClient.EnterAsync(world).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
BIN
banner.png
Normal file
BIN
banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
18
config.example.toml
Normal file
18
config.example.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[VirtualParadise]
|
||||||
|
BotName = ""
|
||||||
|
Username = ""
|
||||||
|
Password = ""
|
||||||
|
World = ""
|
||||||
|
|
||||||
|
[VirtualParadise.Chat]
|
||||||
|
Color = 1644912
|
||||||
|
Style = 0
|
||||||
|
|
||||||
|
[Discord]
|
||||||
|
Token = ""
|
||||||
|
ChannelId = 0
|
||||||
|
|
||||||
|
[Bot]
|
||||||
|
AnnounceAvatarEvents = true
|
||||||
|
AnnounceBots = false
|
||||||
|
RelayBotMessages = false
|
@ -1,16 +1,16 @@
|
|||||||
version: '3.9'
|
version: '3.9'
|
||||||
services:
|
services:
|
||||||
vpbridge:
|
vplink:
|
||||||
container_name: VpBridge
|
container_name: VPLink
|
||||||
pull_policy: build
|
pull_policy: build
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: VpBridge/Dockerfile
|
dockerfile: VPLink/Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /var/log/vp/vp-bridge
|
source: /var/log/vp/vplink
|
||||||
target: /app/logs
|
target: /app/logs
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /etc/vp/vp-bridge
|
source: /etc/vp/vplink
|
||||||
target: /app/data
|
target: /app/data
|
||||||
restart: always
|
restart: always
|
||||||
|
Loading…
Reference in New Issue
Block a user