From 24c8a920e49e3998cc6b2307e577a606713163e5 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:48:04 +0300 Subject: [PATCH] Refactor moderation notifications: replace NotifyAsync with KickPlayerAsync for player kicks in InGameModerationController and AdminController; update NotificationService to streamline moderation kick payloads. --- .../Moderation/InGameModerationController.cs | 9 +-- .../Controllers/Admin/AdminController.cs | 13 ++-- .../Services/NotificationService.cs | 65 +++++++++++-------- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/DorkNet.Server/Controllers/API/Moderation/InGameModerationController.cs b/DorkNet.Server/Controllers/API/Moderation/InGameModerationController.cs index 92f98bc..1408165 100644 --- a/DorkNet.Server/Controllers/API/Moderation/InGameModerationController.cs +++ b/DorkNet.Server/Controllers/API/Moderation/InGameModerationController.cs @@ -136,12 +136,9 @@ public class InGameModerationController( foreach (var target in ids) { logger.LogInformation("[mod] instant-kick by {Caller}: target={Target}", caller, target); - // Push a SignalR ModerationKick notification — the watch's - // PlayerReporting.OnModerationKick handler boots the player - // back to their dorm. - await notifications.NotifyAsync( - target, - Models.Notification.PushNotificationId.ModerationKick); + // Push the ModerationKick shape the 2020.12 client expects: + // Msg.Reason must be a ModerationBlockDetail object. + await notifications.KickPlayerAsync(target, "Instant kick by moderator."); } return Ok(new { success = true, error = "" }); } diff --git a/DorkNet.Server/Controllers/Admin/AdminController.cs b/DorkNet.Server/Controllers/Admin/AdminController.cs index 1a04f3d..0e20725 100644 --- a/DorkNet.Server/Controllers/Admin/AdminController.cs +++ b/DorkNet.Server/Controllers/Admin/AdminController.cs @@ -159,8 +159,8 @@ public class AdminController( await LogAsync("ban_player", "player", id, body.Reason ?? ""); await db.SaveChangesAsync(); - await notifications.NotifyAsync(id, PushNotificationId.ModerationKick, - new { Reason = body.Reason ?? "", Until = player.BannedUntil }); + await notifications.KickPlayerAsync(id, body.Reason ?? "You have been banned."); + playerPresence.Clear(id); return Ok(new { player.Id, player.BannedUntil }); } @@ -1269,7 +1269,7 @@ public class AdminController( { var r = playerPresence.GetRoom(pid); if (r is null || r.RoomId != id || r.RoomInstanceId != instanceId) continue; - await notifications.NotifyAsync(pid, PushNotificationId.ModerationKick, new { Reason = reason }); + await notifications.KickPlayerAsync(pid, reason); playerPresence.Clear(pid); kicked.Add(pid); } @@ -1616,8 +1616,11 @@ public class AdminController( [HttpPost("players/{id:long}/kick")] public async Task KickPlayer(long id, [FromBody] KickRequest? body) { - await notifications.NotifyAsync(id, PushNotificationId.ModerationKick, - new { Reason = body?.Reason ?? "" }); + var reason = string.IsNullOrWhiteSpace(body?.Reason) + ? "Kicked by an admin." + : body!.Reason!.Trim(); + await notifications.KickPlayerAsync(id, reason); + playerPresence.Clear(id); await LogAsync("kick_player", "player", id, body?.Reason ?? ""); await db.SaveChangesAsync(); return Ok(new { kicked = id }); diff --git a/DorkNet.Server/Services/NotificationService.cs b/DorkNet.Server/Services/NotificationService.cs index c5a22b6..030c796 100644 --- a/DorkNet.Server/Services/NotificationService.cs +++ b/DorkNet.Server/Services/NotificationService.cs @@ -329,14 +329,10 @@ public class NotificationService( /// to disconnect via the watch's /// ModerationKick flow. Wire path verified against decompilation: /// PushNotificationId.ModerationKick = 22 is registered - /// at PlayerReporting.txt:244 via the enum-overload, so - /// the wire name resolves to "ModerationKick". - /// OnModerationKick(Dictionary) at - /// PlayerReporting.txt:1857 shows a confirmation dialog - /// then calls LocalModerationKickSelf - /// (PlayerReporting.txt:2647), which queues the - /// disconnect through Core.ShutdownForLogout — the same - /// graceful path the watch uses for real moderator kicks. + /// by LCCEEFHOBEN.OFFLOPLJBBG via the enum overload. + /// LCCEEFHOBEN.KEIAFMLIMFA(Dictionary) reads + /// Msg.Reason as a ModerationBlockDetail object, then + /// calls PODFHPNOHGB / BootLocalPlayerToDormRoom. /// /// Used by /// to enforce single-session-per-account: when a second login on @@ -347,18 +343,11 @@ public class NotificationService( /// public Task KickStaleSession(long playerId, string reason) => NotifyAsync(playerId, PushNotificationId.ModerationKick, - new - { - // Field names follow PlayerReporting's wire convention - // for moderation-related dialogs; the watch's - // OnModerationKick deserializes via Util.GetKeyOrDefault - // so missing fields are tolerated, but populating them - // makes the dialog read sensibly. - Reason = reason, - DisplayReason = reason, - BannedByPlayerId = 1, // Coach - Source = "session_replaced", - }); + ModerationKickPayload(reason, isBan: false, source: "session_replaced")); + + public Task KickPlayerAsync(long playerId, string reason, bool isBan = false) => + NotifyAsync(playerId, PushNotificationId.ModerationKick, + ModerationKickPayload(reason, isBan, source: "admin_kick")); /// Push a ModerationKick to a specific set of SignalR /// connection ids — used by @@ -371,16 +360,38 @@ public class NotificationService( public Task KickConnectionsAsync(IReadOnlyCollection connectionIds, string reason) { if (connectionIds.Count == 0) return Task.CompletedTask; - var payload = Serialize(PushNotificationId.ModerationKick, new - { - Reason = reason, - DisplayReason = reason, - BannedByPlayerId = 1, - Source = "session_replaced", - }); + var payload = Serialize(PushNotificationId.ModerationKick, + ModerationKickPayload(reason, isBan: false, source: "session_replaced")); return hub.Clients.Clients(connectionIds).SendAsync(HubMethod, payload); } + private static object ModerationKickPayload(string reason, bool isBan, string source) + { + var message = string.IsNullOrWhiteSpace(reason) + ? "Kicked to Dormroom" + : reason.Trim(); + + var blockDetail = new + { + ReportCategory = 10, // PlayerReporting.ReportCategory.VoteKick + Duration = 0, + GameSessionId = 0L, + Message = message, + IsHostKick = true, + PlayerIdReporter = 1, + IsBan = isBan, + VoteKickReason = message, + }; + + return new + { + Reason = blockDetail, + DisplayReason = message, + BannedByPlayerId = 1, + Source = source, + }; + } + /// Notify a private-instance owner that someone tried to /// follow them in via "Go to Player" but was rejected for not /// being on the invite list. Reuses the MessageReceived channel