mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Update notification handling to prevent account DTO issues; add SystemNotificationService for event notifications
This commit is contained in:
@@ -332,17 +332,13 @@ public class AvatarGiftsController(
|
|||||||
gift.ConsumedAt = DateTime.UtcNow;
|
gift.ConsumedAt = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Profile-update push so the watch's wallet readout refreshes
|
// The wallet readout refreshes off the StorefrontBalanceUpdate that
|
||||||
// without waiting for the next /balance poll. Mirrors what
|
// GrantCurrencyAsync (above) already pushes for currency gifts. Do
|
||||||
// /api/admin/v1/players/{id}/gift does after the inbox push so
|
// NOT also push SubscriptionUpdateProfile ("AccountUpdate") with a
|
||||||
// the toast number animates up the moment the box closes.
|
// {Reason,Currency,…} blob: the 2023 client deserialises that channel
|
||||||
if (gift.CurrencyType > 0 || gift.Xp > 0 || gift.Level > 0)
|
// as its Account DTO, so a non-account payload decodes to a blank
|
||||||
{
|
// account (accountId 0) → "Orphan player account (0) found" spam
|
||||||
await notifications.NotifyAsync(pid,
|
// (audited 2026-07-12).
|
||||||
PushNotificationId.SubscriptionUpdateProfile,
|
|
||||||
new { Reason = "GiftConsumed", gift.CurrencyType, gift.Currency, gift.Xp, gift.Level });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(ToWire(gift));
|
return Ok(ToWire(gift));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1054,11 +1054,12 @@ public class RoomsController(
|
|||||||
EncryptVoiceChat = currentPresence.EncryptVoiceChat,
|
EncryptVoiceChat = currentPresence.EncryptVoiceChat,
|
||||||
},
|
},
|
||||||
isOnline = true,
|
isOnline = true,
|
||||||
// 2023.03.21 client build version (baked "20230317"). MUST match
|
// 2023.03.21 client build version. MUST be the STRING
|
||||||
// the joining client's own version or it reports a version
|
// "20230317" — the client's presence DTO reads appVersion
|
||||||
// mismatch and refuses to join. Was a stale December-2020 value
|
// with a string reader, so an int crashes the SignalR
|
||||||
// (20201210) carried over when this branch forked.
|
// message ("expected:'String Begin Token'") and aborts the
|
||||||
appVersion = 20230317,
|
// update. Must also match the joining client's version.
|
||||||
|
appVersion = "20230317",
|
||||||
};
|
};
|
||||||
await notifications.NotifyAsync(playerId, PushNotificationId.SubscriptionUpdatePresence, presencePayload);
|
await notifications.NotifyAsync(playerId, PushNotificationId.SubscriptionUpdatePresence, presencePayload);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,19 +43,17 @@ public class LevelService(DorkNetDbContext db, NotificationService notifications
|
|||||||
|
|
||||||
if (player.Level > oldLevel)
|
if (player.Level > oldLevel)
|
||||||
{
|
{
|
||||||
// Level-up grants a small currency reward and a push so
|
// Level-up grants a small currency reward. The wallet UI
|
||||||
// the watch can play its level-up animation.
|
// refreshes off the StorefrontBalanceUpdate that
|
||||||
|
// GrantCurrencyAsync already pushes — do NOT also push
|
||||||
|
// SubscriptionUpdateProfile ("AccountUpdate") with a
|
||||||
|
// {Reason,Level} blob: the 2023 client deserialises that
|
||||||
|
// channel as its Account DTO, so a non-account payload
|
||||||
|
// decodes to a blank account (accountId 0) and logs
|
||||||
|
// "Orphan player account (0) found" (audited 2026-07-12).
|
||||||
await GrantCurrencyAsync(playerId, currencyType: 2,
|
await GrantCurrencyAsync(playerId, currencyType: 2,
|
||||||
amount: 100 * (player.Level - oldLevel),
|
amount: 100 * (player.Level - oldLevel),
|
||||||
reason: $"levelup:{oldLevel}->{player.Level}");
|
reason: $"levelup:{oldLevel}->{player.Level}");
|
||||||
await notifications.NotifyAsync(playerId,
|
|
||||||
PushNotificationId.SubscriptionUpdateProfile,
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Reason = "LevelUp",
|
|
||||||
Level = player.Level,
|
|
||||||
PreviousLevel = oldLevel,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (player.Level, player.XP);
|
return (player.Level, player.XP);
|
||||||
|
|||||||
@@ -298,10 +298,14 @@ public class NotificationService(
|
|||||||
},
|
},
|
||||||
// 2020.12 PlayerPresence.Deserialize requires these.
|
// 2020.12 PlayerPresence.Deserialize requires these.
|
||||||
["isOnline"] = redacted is not null,
|
["isOnline"] = redacted is not null,
|
||||||
// 2023.03.21 client build version ("20230317"); must match the
|
// 2023.03.21 client build version; must match the joining
|
||||||
// joining client's own version or it reports a version mismatch.
|
// client's own version or it reports a version mismatch. MUST be
|
||||||
// Stale 2020 value (20201210) carried over from the December branch.
|
// a STRING ("20230317"): the client's presence DTO reads
|
||||||
["appVersion"] = 20230317,
|
// appVersion with a string reader, so an int crashes the whole
|
||||||
|
// SignalR message with "expected:'String Begin Token',
|
||||||
|
// actual:'20230317'" — which aborts the presence/room update
|
||||||
|
// (e.g. surfaced as "failed to create a new room").
|
||||||
|
["appVersion"] = "20230317",
|
||||||
};
|
};
|
||||||
|
|
||||||
var envelope = new System.Text.Json.Nodes.JsonObject
|
var envelope = new System.Text.Json.Nodes.JsonObject
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,6 +201,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<ClubService>();
|
services.AddScoped<ClubService>();
|
||||||
services.AddScoped<LevelService>();
|
services.AddScoped<LevelService>();
|
||||||
services.AddScoped<StoreService>();
|
services.AddScoped<StoreService>();
|
||||||
|
services.AddScoped<SystemNotificationService>();
|
||||||
services.AddScoped<QuestRewardService>();
|
services.AddScoped<QuestRewardService>();
|
||||||
services.AddScoped<SignupCodeService>();
|
services.AddScoped<SignupCodeService>();
|
||||||
// Singleton — the protobuf blob is identical for every room and built
|
// Singleton — the protobuf blob is identical for every room and built
|
||||||
|
|||||||
Reference in New Issue
Block a user