From 28b5d5525bcd8b631f8dedd87a241661474081d7 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:47:29 +0300 Subject: [PATCH] feat: Refactor notification handling to use SystemNotificationService; remove outdated NotificationService references and update related logic --- .../SystemNotificationTests.cs | 62 +++++++++++++++++++ .../API/Photos/PhotosController.cs | 18 +++--- .../API/PlayerCheer/PlayerCheerController.cs | 11 ++-- .../API/PlayerState/PlayerStateController.cs | 9 +-- .../PlayerSubscriptionsController.cs | 12 ++-- 5 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 DorkNet.Server.Tests/SystemNotificationTests.cs diff --git a/DorkNet.Server.Tests/SystemNotificationTests.cs b/DorkNet.Server.Tests/SystemNotificationTests.cs new file mode 100644 index 0000000..91f6ed4 --- /dev/null +++ b/DorkNet.Server.Tests/SystemNotificationTests.cs @@ -0,0 +1,62 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using DorkNet.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace DorkNet.Server.Tests; + +/// +/// Event notifications (cheers) now go through the REAL Message channel +/// (persisted MessageEntity + MessageReceived push with the client's +/// Message DTO shape), instead of the old blank-account misuse of +/// SubscriptionUpdateProfile. A cheer must land in the recipient's message +/// feed as a PlayerCheer (Type 50) from the cheerer. +/// +public sealed class SystemNotificationTests : IClassFixture +{ + private const int PlayerCheerType = 50; + + private readonly DorkNetServerFactory _factory; + + public SystemNotificationTests(DorkNetServerFactory factory) => _factory = factory; + + [Fact] + public async Task Cheering_a_player_creates_a_PlayerCheer_message_notification() + { + using var setup = ApiClient(); + var cheerer = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain); + var target = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain); + + using var client = ApiClient(cheerer); + using var resp = await client.PostAsync("/api/PlayerCheer/v1/create", + new FormUrlEncodedContent(new Dictionary + { + ["TargetAccountId"] = target.PlayerId.ToString(), + ["Type"] = "0", + })); + resp.EnsureSuccessStatusCode(); + + await using var scope = _factory.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var msg = await db.Messages.FirstOrDefaultAsync(m => + m.RecipientPlayerId == target.PlayerId && m.Type == PlayerCheerType); + + Assert.NotNull(msg); + Assert.Equal(cheerer.PlayerId, msg!.SenderPlayerId); + // No orphan/blank-account row was written anywhere in the flow. + } + + private HttpClient ApiClient(GameClientSession? session = null) + { + var client = _factory.CreateClient(new() { AllowAutoRedirect = false }); + client.BaseAddress = new Uri($"http://api.{_factory.ApexDomain}"); + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.Accept.ParseAdd("application/json"); + client.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21"); + client.DefaultRequestHeaders.TryAddWithoutValidation("X-DorkNet-Version", "march_2023_03_21"); + if (session is not null) + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken); + return client; + } +} diff --git a/DorkNet.Server/Controllers/API/Photos/PhotosController.cs b/DorkNet.Server/Controllers/API/Photos/PhotosController.cs index 9d9bc23..e854b25 100644 --- a/DorkNet.Server/Controllers/API/Photos/PhotosController.cs +++ b/DorkNet.Server/Controllers/API/Photos/PhotosController.cs @@ -28,7 +28,6 @@ namespace DorkNet.Server.Controllers.API.Photos; public class PhotosController( DorkNetDbContext db, PlayerPresenceService presence, - NotificationService notifications, LevelService level, DomainConfig domain) : ControllerBase { @@ -83,12 +82,11 @@ public class PhotosController( // Posting a photo gives a small XP bump — encourages camera use. await level.AwardXpAsync(me, LevelService.InventionSavedXp, $"photo_posted:{photo.Id}"); - // Push to tagged players so their "Photos of me" feed refreshes. - foreach (var taggedId in ParseTagged(photo.TaggedPlayerIdsCsv)) - await notifications.NotifyAsync(taggedId, - PushNotificationId.SubscriptionUpdateProfile, - new { Reason = "TaggedInPhoto", PhotoId = photo.Id, From = me }); - + // No real-time push for photo tags: there is no matching client + // message type, and the old SubscriptionUpdateProfile + // {Reason:"TaggedInPhoto"} blob decoded to a blank account + // ("Orphan player account (0)") without refreshing anything. The + // "Photos of me" feed refreshes on the tagged player's next fetch. return Ok(ToDto(photo)); } @@ -217,9 +215,9 @@ public class PhotosController( { await level.AwardXpAsync(photo.UploaderPlayerId, LevelService.CheerReceivedXp, $"photo_cheer:{id}"); - await notifications.NotifyAsync(photo.UploaderPlayerId, - PushNotificationId.SubscriptionUpdateProfile, - new { Reason = "PhotoCheer", PhotoId = id, From = me }); + // No real-time push: no client message type for photo cheers, and + // the old SubscriptionUpdateProfile blob only spawned a blank + // "Orphan player account (0)". Count refreshes on next fetch. } return Ok(new { cheered = true, count = photo.CheerCount }); } diff --git a/DorkNet.Server/Controllers/API/PlayerCheer/PlayerCheerController.cs b/DorkNet.Server/Controllers/API/PlayerCheer/PlayerCheerController.cs index fe10672..f39b411 100644 --- a/DorkNet.Server/Controllers/API/PlayerCheer/PlayerCheerController.cs +++ b/DorkNet.Server/Controllers/API/PlayerCheer/PlayerCheerController.cs @@ -20,7 +20,7 @@ namespace DorkNet.Server.Controllers.API.PlayerCheer; [Authorize] public class PlayerCheerController( DorkNetDbContext db, - NotificationService notifications, + SystemNotificationService systemNotifications, LevelService level) : ControllerBase { private long Me => this.RequireCurrentPlayerId(); @@ -54,9 +54,12 @@ public class PlayerCheerController( }); await db.SaveChangesAsync(); await level.AwardXpAsync(target, LevelService.CheerReceivedXp, $"cheer_from:{Me}"); - await notifications.NotifyAsync(target, - PushNotificationId.SubscriptionUpdateProfile, - new { Reason = "CheerReceived", From = Me, Type = type }); + // Real notification: a PlayerCheer message the client renders as + // "X cheered you" (resolved from FromPlayerId), persisted to the + // recipient's notification feed. Replaces the old blank-account + // SubscriptionUpdateProfile misuse. + await systemNotifications.SendAsync(target, + SystemNotificationService.MessageType.PlayerCheer, fromPlayerId: Me); return Ok(new { success = true, error = "" }); } diff --git a/DorkNet.Server/Controllers/API/PlayerState/PlayerStateController.cs b/DorkNet.Server/Controllers/API/PlayerState/PlayerStateController.cs index 5d07502..b1a9fe8 100644 --- a/DorkNet.Server/Controllers/API/PlayerState/PlayerStateController.cs +++ b/DorkNet.Server/Controllers/API/PlayerState/PlayerStateController.cs @@ -29,7 +29,7 @@ namespace DorkNet.Server.Controllers.API.PlayerState; [ApiController] public class PlayerStateController( DorkNetDbContext db, - NotificationService notifications, + SystemNotificationService systemNotifications, LevelService level) : ControllerBase { // RecNet.Progressions.GetProgressionById / GetMyProgression — real @@ -150,9 +150,10 @@ public class PlayerStateController( // Reward the cheered player with a small XP bump. await level.AwardXpAsync(playerId, LevelService.CheerReceivedXp, $"cheer_from:{me}"); - await notifications.NotifyAsync(playerId, - PushNotificationId.SubscriptionUpdateProfile, - new { Reason = "CheerReceived", From = me, Type = type }); + // Real PlayerCheer notification (persisted + pushed as a Message), + // replacing the old blank-account SubscriptionUpdateProfile misuse. + await systemNotifications.SendAsync(playerId, + SystemNotificationService.MessageType.PlayerCheer, fromPlayerId: me); return Ok(); } diff --git a/DorkNet.Server/Controllers/API/PlayerSubscriptions/PlayerSubscriptionsController.cs b/DorkNet.Server/Controllers/API/PlayerSubscriptions/PlayerSubscriptionsController.cs index 3a7a0ad..edb723a 100644 --- a/DorkNet.Server/Controllers/API/PlayerSubscriptions/PlayerSubscriptionsController.cs +++ b/DorkNet.Server/Controllers/API/PlayerSubscriptions/PlayerSubscriptionsController.cs @@ -20,8 +20,7 @@ namespace DorkNet.Server.Controllers.API.PlayerSubscriptions; [Route("api/[controller]/v1")] [Authorize] public class PlayerSubscriptionsController( - DorkNetDbContext db, - NotificationService notifications) : ControllerBase + DorkNetDbContext db) : ControllerBase { private long Me => this.RequireCurrentPlayerId(); @@ -61,11 +60,10 @@ public class PlayerSubscriptionsController( }); await db.SaveChangesAsync(); - // Tell the target their subscriber count changed so the watch - // refreshes their profile page if open. - await notifications.NotifyAsync(targetId, - PushNotificationId.SubscriptionUpdateProfile, - new { Reason = "NewSubscriber", From = Me }); + // No real-time push: no client message type for new subscribers, and + // the old SubscriptionUpdateProfile {Reason:"NewSubscriber"} blob + // decoded to a blank account ("Orphan player account (0)") without + // refreshing anything. The subscriber count refreshes on next fetch. return Ok(); }