Files
DorkNet/DorkNet.Server/Services/SystemNotificationService.cs
Alexa e33145307f fix: notification pushes now match what the 2023 client reads
Most pushes were still shaped for the 2020 watch, so the 2023 client
parsed them as empty or dropped them without any error.

- RelationshipChanged carries the relationship row, to both players, on
  friend request / accept / remove / add and on favorite / mute / ignore.
  Friend requests and accepts also land in the notifications inbox.
- ModerationKick uses the client's flat moderation block, with the
  reason in VoteKickReason (the only field the dialog shows).
- Bans, account deletion, password resets and second-session takeover
  are a Logout with a numeric reason instead of a kick to the dorm; the
  old string reason failed the client's parse.
- SubscribeToPlayers is honoured: each connection joins a presence group
  per subscribed player, and presence (only presence) reaches subscribers
  as well as friends. The hub allows 180 s of client silence, since the
  client reconnects at most 3 times per login.
- Admin broadcast sends its text as a coach message and can start the
  client's own maintenance countdown; ServerMaintenance only reads
  StartsInMinutes.
- ConsumableMapping, StorefrontBalance, gift, player event, level-up,
  RoomUpdate, profile-image and PlayerLeftChat pushes carry the client's
  DTOs. "Join rejected" is the client's RequestGameInvite message.

Contract in docs/recroom-2023-notifications.md; push frames are checked
by NotificationContractTests through a capturing hub context.
2026-10-24 19:50:05 +03:00

89 lines
3.6 KiB
C#

using DorkNet.Models.Notification;
using DorkNet.Server.Data;
using DorkNet.Server.Data.Entities;
namespace DorkNet.Server.Services;
/// <summary>
/// The REAL channel for event notifications (cheers, friend events, room
/// co-owner changes, …). A notification is a <see cref="MessageEntity"/>
/// with a <see cref="MessageType"/> discriminator: it persists to the
/// recipient's message/notification feed (so <c>GET api/messages</c>
/// returns it) and is pushed live via <c>MessageReceived</c> with the
/// client's Message DTO shape (RecNet.Runtime <c>KJECOLODAFA</c>:
/// Id/FromPlayerId/SentTime/Type/Data/RoomId/PlayerEventId).
///
/// This replaces the old anti-pattern of pushing <c>{Reason:"…"}</c> blobs
/// on the <c>SubscriptionUpdateProfile</c> ("AccountUpdate") channel — the
/// 2023 client decoded those as its Account DTO, producing a blank account
/// (accountId 0) and "Orphan player account (0) found" log spam, and never
/// rendered the notification. Every field type here matches the client
/// reader (FromPlayerId is Int32; SentTime is an ISO DateTime; Type/RoomId/
/// PlayerEventId are number/nullable-number).
/// </summary>
public sealed class SystemNotificationService(DorkNetDbContext db, NotificationService notifications)
{
/// <summary>RecNet message-type enum (<c>KJECOLODAFA+MEPCALGGMJC</c>),
/// verified from the 2023.03.21 client. Only the values we send are
/// listed; add more as needed.</summary>
public static class MessageType
{
public const int FriendInvite = 4;
public const int RequestGameInvite = 10;
public const int TextMessage = 30;
public const int FriendRequestAccepted = 40;
public const int FriendCodeUsed = 41;
public const int PlayerCheer = 50;
public const int PlayerCheerAnonymous = 51;
public const int RoomCoOwnerAdded = 60;
public const int RoomCoOwnerRemoved = 61;
public const int RoomCoOwnerInvited = 62;
public const int CreatorPublishedNewRoom = 70;
public const int PlayerAttendingEvent = 80;
public const int PlayerEventInvitation = 81;
public const int CoachMessage = 100;
public const int FriendIntroduction = 130;
}
/// <summary>Persist and push a system notification to
/// <paramref name="recipientPlayerId"/>. <paramref name="fromPlayerId"/>
/// is the actor the client resolves the name/avatar from (0 for a
/// system/anonymous notification). <paramref name="data"/> is the
/// type-specific payload string (empty when the Type + FromPlayerId are
/// sufficient, e.g. a cheer). Returns the created message id.</summary>
public async Task<long> SendAsync(
long recipientPlayerId,
int type,
long fromPlayerId = 0,
string data = "",
long? roomId = null)
{
if (recipientPlayerId <= 0) return 0;
var msg = new MessageEntity
{
SenderPlayerId = fromPlayerId,
RecipientPlayerId = recipientPlayerId,
Body = data ?? string.Empty,
Type = type,
RoomId = roomId,
SentAt = DateTime.UtcNow,
};
db.Messages.Add(msg);
await db.SaveChangesAsync();
await notifications.NotifyAsync(recipientPlayerId, PushNotificationId.MessageReceived, new
{
Id = msg.Id,
// Client Message DTO reads FromPlayerId as Int32.
FromPlayerId = unchecked((int)fromPlayerId),
SentTime = msg.SentAt,
Type = type,
Data = msg.Body,
RoomId = roomId,
PlayerEventId = (long?)null,
});
return msg.Id;
}
}