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();
}