From c07d91fdb2026232a176761afa5f46abeb73c837 Mon Sep 17 00:00:00 2001 From: Blake LaFleur Date: Sat, 30 Aug 2025 21:26:49 -0500 Subject: [PATCH] Add support for area server events and discord webhook notifications --- .../Contexts/GuildShopContextAccessor.cs | 2 - .../Events/AreaServerPublishedEvent.cs | 15 +++++ .../AreaServerPublishedEventHandler.cs | 27 ++++++++ .../AreaServerStatusUpdateEventHandler.cs | 29 +++++++++ .../Fragment.NetSlum.Networking.csproj | 4 ++ .../AreaServerIPAddressPortRequest.cs | 2 +- .../AreaServerPublishDetailsRequest.cs | 11 +++- .../Notifications/NotificationOptions.cs | 16 +++++ .../Notifications/NotificationService.cs | 61 +++++++++++++++++++ .../Providers/AbstractNotificationProvider.cs | 18 ++++++ .../Providers/BaseNotificationOptions.cs | 6 ++ .../DiscordAbstractNotificationProvider.cs | 21 +++++++ .../Discord/DiscordNotificationOptions.cs | 6 ++ src/Fragment.NetSlum.Server/Program.cs | 8 +++ src/Fragment.NetSlum.Server/serverConfig.json | 5 +- 15 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 src/Fragment.NetSlum.Networking/Events/AreaServerPublishedEvent.cs create mode 100644 src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerPublishedEventHandler.cs create mode 100644 src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerStatusUpdateEventHandler.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/NotificationOptions.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/NotificationService.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/Providers/AbstractNotificationProvider.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/Providers/BaseNotificationOptions.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordAbstractNotificationProvider.cs create mode 100644 src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordNotificationOptions.cs diff --git a/src/Fragment.NetSlum.Networking/Contexts/GuildShopContextAccessor.cs b/src/Fragment.NetSlum.Networking/Contexts/GuildShopContextAccessor.cs index 4dd1edd..6e63b40 100644 --- a/src/Fragment.NetSlum.Networking/Contexts/GuildShopContextAccessor.cs +++ b/src/Fragment.NetSlum.Networking/Contexts/GuildShopContextAccessor.cs @@ -1,5 +1,3 @@ -using Fragment.NetSlum.Persistence.Entities; - namespace Fragment.NetSlum.Networking.Contexts; /// diff --git a/src/Fragment.NetSlum.Networking/Events/AreaServerPublishedEvent.cs b/src/Fragment.NetSlum.Networking/Events/AreaServerPublishedEvent.cs new file mode 100644 index 0000000..158f250 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Events/AreaServerPublishedEvent.cs @@ -0,0 +1,15 @@ +using Fragment.NetSlum.Core.CommandBus.Contracts.Events; +using Fragment.NetSlum.Networking.Models; + +namespace Fragment.NetSlum.Networking.Events; + +public class AreaServerPublishedEvent : IEvent +{ + public AreaServerPublishedEvent(AreaServerInformation areaServerInfo) + { + AreaServerInfo = areaServerInfo; + } + + public AreaServerInformation AreaServerInfo { get; } + +} diff --git a/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerPublishedEventHandler.cs b/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerPublishedEventHandler.cs new file mode 100644 index 0000000..66e4048 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerPublishedEventHandler.cs @@ -0,0 +1,27 @@ +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Fragment.NetSlum.Core.CommandBus.Contracts.Events; +using Fragment.NetSlum.Networking.Services.Notifications; + +namespace Fragment.NetSlum.Networking.Events.Handlers; + +public class AreaServerPublishedEventHandler : EventHandler +{ + private readonly NotificationService _notificationService; + + public AreaServerPublishedEventHandler(NotificationService notificationService) + { + _notificationService = notificationService; + } + + public override async ValueTask Handle(AreaServerPublishedEvent eventInfo, CancellationToken cancellationToken) + { + var sb = new StringBuilder(); + sb.AppendLine($"Name: {eventInfo.AreaServerInfo.ServerName}"); + sb.AppendLine($"Level: {eventInfo.AreaServerInfo.Level}"); + + await _notificationService.SendNotification(new NotificationService.NotificationMessage( + NotificationService.NotificationType.AreaServer, "Area Server Published", sb.ToString())); + } +} diff --git a/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerStatusUpdateEventHandler.cs b/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerStatusUpdateEventHandler.cs new file mode 100644 index 0000000..b5069d4 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Events/Handlers/AreaServerStatusUpdateEventHandler.cs @@ -0,0 +1,29 @@ +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Fragment.NetSlum.Core.CommandBus.Contracts.Events; +using Fragment.NetSlum.Networking.Services.Notifications; + +namespace Fragment.NetSlum.Networking.Events.Handlers; + +public class AreaServerStatusUpdateEventHandler : EventHandler +{ + private readonly NotificationService _notificationService; + + public AreaServerStatusUpdateEventHandler(NotificationService notificationService) + { + _notificationService = notificationService; + } + + public override async ValueTask Handle(AreaServerStatusUpdateEvent eventInfo, CancellationToken cancellationToken) + { + var sb = new StringBuilder(); + sb.AppendLine($"Name: {eventInfo.ServerName}"); + sb.AppendLine($"Level: {eventInfo.Level}"); + sb.AppendLine($"State: {eventInfo.State.ToString()}"); + sb.AppendLine($"Player Count: {eventInfo.CurrentPlayerCount}"); + + await _notificationService.SendNotification(new NotificationService.NotificationMessage( + NotificationService.NotificationType.AreaServer, "Area Server Status Changed", sb.ToString())); + } +} diff --git a/src/Fragment.NetSlum.Networking/Fragment.NetSlum.Networking.csproj b/src/Fragment.NetSlum.Networking/Fragment.NetSlum.Networking.csproj index 4929e77..68fa433 100644 --- a/src/Fragment.NetSlum.Networking/Fragment.NetSlum.Networking.csproj +++ b/src/Fragment.NetSlum.Networking/Fragment.NetSlum.Networking.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerIPAddressPortRequest.cs b/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerIPAddressPortRequest.cs index 08e81c4..cd1f4fe 100644 --- a/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerIPAddressPortRequest.cs +++ b/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerIPAddressPortRequest.cs @@ -45,7 +45,7 @@ public override ValueTask> GetResponse(FragmentTcpS session.AreaServerInfo!.PublicConnectionEndpoint = new IPEndPoint( asIpAddress, BinaryPrimitives.ReadUInt16BigEndian(request.Data[4..6].Span)); - _logger.LogInformation("{AreaServerInfo}", session.AreaServerInfo!.ToString()); + _logger.LogInformation("Area Server Published Connection Info: {NewLine}{AreaServerInfo}", Environment.NewLine, session.AreaServerInfo!.ToString()); BaseResponse response = new AreaServerIPAddressPortResponse(); return SingleMessage(response.Build()); diff --git a/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerPublishDetailsRequest.cs b/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerPublishDetailsRequest.cs index 6cd15ab..e56848e 100644 --- a/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerPublishDetailsRequest.cs +++ b/src/Fragment.NetSlum.Networking/Packets/Request/AreaServer/AreaServerPublishDetailsRequest.cs @@ -1,10 +1,13 @@ +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Threading.Tasks; +using Fragment.NetSlum.Core.CommandBus; using Fragment.NetSlum.Core.Constants; using Fragment.NetSlum.Core.Extensions; using Fragment.NetSlum.Networking.Attributes; using Fragment.NetSlum.Networking.Constants; +using Fragment.NetSlum.Networking.Events; using Fragment.NetSlum.Networking.Objects; using Fragment.NetSlum.Networking.Packets.Response.AreaServer; using Fragment.NetSlum.Networking.Packets.Response; @@ -22,11 +25,13 @@ public class AreaServerPublishDetailsRequest:BaseRequest { private readonly FragmentContext _database; private readonly ILogger _logger; + private readonly ICommandBus _commandBus; - public AreaServerPublishDetailsRequest(FragmentContext database, ILogger logger) + public AreaServerPublishDetailsRequest(FragmentContext database, ILogger logger, ICommandBus commandBus) { _database = database; _logger = logger; + _commandBus = commandBus; } public override ValueTask> GetResponse(FragmentTcpSession session, FragmentMessage request) @@ -50,6 +55,10 @@ public override ValueTask> GetResponse(FragmentTcpS pos += 3; session.AreaServerInfo.ServerId = request.Data[pos..]; + _logger.LogInformation("Area Server Published Details: {NewLine}{AreaServerInfo}", Environment.NewLine, session.AreaServerInfo!.ToString()); + + _commandBus.Notify(new AreaServerPublishedEvent(session.AreaServerInfo!)).Wait(); + response = new AreaServerPublishDetailsResponse { PacketType = OpCodes.Data_AreaServerPublishDetails1Success, Data = [0x00, 0x01 ] }; diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationOptions.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationOptions.cs new file mode 100644 index 0000000..46e9ee9 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Fragment.NetSlum.Networking.Services.Notifications; + +public class NotificationOptions +{ + public class NotificationDestination + { + public required string ProviderName { get; set; } + public Dictionary Options { get; set; } = new Dictionary(); + + public ICollection Types { get; set; } = [NotificationService.NotificationType.All]; + } + + public ICollection Destinations { get; set; } = []; +} diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationService.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationService.cs new file mode 100644 index 0000000..b7abcee --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/NotificationService.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Fragment.NetSlum.Networking.Services.Notifications.Providers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Fragment.NetSlum.Networking.Services.Notifications; + +public class NotificationService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IOptionsMonitor _optionsMonitor; + + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() }, + }; + + [Flags] + public enum NotificationType + { + AreaServer = 1 << 0, + Character = 1 << 1, + All = AreaServer | Character, + } + + public record NotificationMessage(NotificationType Type, string Title, string Content); + + public NotificationService(IServiceScopeFactory scopeFactory, IOptionsMonitor optionsMonitor) + { + _scopeFactory = scopeFactory; + _optionsMonitor = optionsMonitor; + } + + public async Task SendNotification(NotificationMessage message) + { + using var scope = _scopeFactory.CreateScope(); + + var configuredNotifications = _optionsMonitor.CurrentValue.Destinations + .Where(n => n.Types.Any(t => t.HasFlag(message.Type))) + .ToList(); + + foreach (var destination in configuredNotifications) + { + var provider = scope.ServiceProvider.GetRequiredKeyedService(destination.ProviderName); + + var optionsType = provider.GetType().BaseType!.GetGenericArguments()[0]; + + var jsonOptions = JsonSerializer.Serialize(destination.Options, SerializerOptions); + + await provider.Send(message, JsonSerializer.Deserialize(jsonOptions, optionsType, SerializerOptions)!); + } + + } +} diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/AbstractNotificationProvider.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/AbstractNotificationProvider.cs new file mode 100644 index 0000000..6606bfd --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/AbstractNotificationProvider.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Fragment.NetSlum.Networking.Services.Notifications.Providers; + +public interface INotificationProvider +{ + public Task Send(NotificationService.NotificationMessage message, object options); +} + +public abstract class AbstractNotificationProvider : INotificationProvider where TOptions : BaseNotificationOptions +{ + protected abstract Task SendNotification(NotificationService.NotificationMessage message, TOptions options); + + public async Task Send(NotificationService.NotificationMessage message, object options) + { + await SendNotification(message, (TOptions)options); + } +} diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/BaseNotificationOptions.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/BaseNotificationOptions.cs new file mode 100644 index 0000000..47858df --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/BaseNotificationOptions.cs @@ -0,0 +1,6 @@ +namespace Fragment.NetSlum.Networking.Services.Notifications.Providers; + +public class BaseNotificationOptions +{ + +} diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordAbstractNotificationProvider.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordAbstractNotificationProvider.cs new file mode 100644 index 0000000..baebcb6 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordAbstractNotificationProvider.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Discord; +using Discord.Webhook; + +namespace Fragment.NetSlum.Networking.Services.Notifications.Providers.Discord; + +public class DiscordNotificationProvider : AbstractNotificationProvider +{ + protected override async Task SendNotification(NotificationService.NotificationMessage message, DiscordNotificationOptions options) + { + using var discordClient = new DiscordWebhookClient(options.WebhookUri); + + var embed = new EmbedBuilder + { + Title = message.Title, + Description = message.Content, + }; + + await discordClient.SendMessageAsync(embeds: [embed.Build()]); + } +} diff --git a/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordNotificationOptions.cs b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordNotificationOptions.cs new file mode 100644 index 0000000..ef08526 --- /dev/null +++ b/src/Fragment.NetSlum.Networking/Services/Notifications/Providers/Discord/DiscordNotificationOptions.cs @@ -0,0 +1,6 @@ +namespace Fragment.NetSlum.Networking.Services.Notifications.Providers.Discord; + +public class DiscordNotificationOptions : BaseNotificationOptions +{ + public string WebhookUri { get; set; } = default!; +} diff --git a/src/Fragment.NetSlum.Server/Program.cs b/src/Fragment.NetSlum.Server/Program.cs index ee4f445..2c132a9 100644 --- a/src/Fragment.NetSlum.Server/Program.cs +++ b/src/Fragment.NetSlum.Server/Program.cs @@ -12,6 +12,9 @@ using Fragment.NetSlum.Core.CommandBus; using Fragment.NetSlum.Networking.Contexts; using Fragment.NetSlum.Networking.Extensions; +using Fragment.NetSlum.Networking.Services.Notifications; +using Fragment.NetSlum.Networking.Services.Notifications.Providers; +using Fragment.NetSlum.Networking.Services.Notifications.Providers.Discord; using Fragment.NetSlum.Networking.Stores; using Fragment.NetSlum.Persistence; using Fragment.NetSlum.Persistence.Extensions; @@ -178,6 +181,11 @@ builder.Services.AddHostedService(); builder.Services.AddScoped(); +builder.Services.Configure(builder.Configuration.GetSection("Notifications")); + +builder.Services.AddKeyedScoped(nameof(DiscordNotificationProvider).Replace("NotificationProvider", string.Empty, StringComparison.InvariantCultureIgnoreCase)); +builder.Services.AddSingleton(); + var app = builder.Build(); diff --git a/src/Fragment.NetSlum.Server/serverConfig.json b/src/Fragment.NetSlum.Server/serverConfig.json index b6cec6b..6063f7f 100644 --- a/src/Fragment.NetSlum.Server/serverConfig.json +++ b/src/Fragment.NetSlum.Server/serverConfig.json @@ -41,5 +41,8 @@ "Authentication": { "JwtSecret": "changeme123" }, - "AllowedOrigins": "http://localhost:3000,https://fragment.psrewired.com" + "Notifications": { + "Destinations": [] + }, + "AllowedOrigins": "http://localhost:3000,https://fragment.psrewired.com,https://fragment.dothackers.org" }