Files
DorkNet/docs/recroom-2023-notifications.md
T
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

9.5 KiB

2023 client notifications (realtime push)

The contract between DorkNet and the 2023-03-21 client's notification hub. Sources: the client's RecNet.Runtime ISIL (C:\tmp\recroom-2023-03-21-isil, obfuscated names given as found there), cross-checked against RecFlare (build 20230414) where useful. Code: DorkNet.Server/Hubs/NotifyHub.cs, Services/NotificationService.cs, Services/PresenceBroadcastService.cs, Services/SystemNotificationService.cs. Tests: DorkNet.Server.Tests/NotificationContractTests.cs.

Connection

  • The hub is {notify host}/hub/v1 (nameserver key Notifications). The client opens it once, at login.
  • Negotiate: POST …/hub/v1/negotiate, no negotiateVersion. The client reads connectionId and availableTransports, and accepts only WebSockets.
  • WebSocket: …/hub/v1?id=<connectionId>, JSON protocol ({"protocol":"json","version":1}), no MessagePack.
  • Auth: Authorization: Bearer on both negotiate and the upgrade. No access_token query string.
  • Keep-alive: the client pings every 15 s from its main thread and has no server timeout of its own. The server pings every 10 s and allows 180 s of client silence, because a long room-load hitch pauses the client's pings.
  • Reconnects: at most 3 per login. The client's attempt counter is never reset after a successful reconnect. Once the budget is spent the hub stays dead, with no message, until the next login. The server must never close a healthy connection.

Envelope

Every push invokes the hub method Notification with one string argument, whose content is JSON:

{"Id":"<key>","Msg":{...}}
  • Id is a JSON string. It is looked up case-sensitively in the client's handler table, and a miss drops the push silently.
    • Handlers registered through the enum overload use the decimal string of the id ("2").
    • The rest use a name (see the table below).
    • NotificationService.WireName maps PushNotificationId to the key.
  • Msg is an inline JSON object of the handler's DTO, never JSON-in-a-string.
    • It is parsed strictly. Keys may be PascalCase, camelCase or all lowercase, and unknown keys are skipped.
    • A value of the wrong type (a number where a string is expected, a string where an enum number is expected) throws, and the whole push is dropped.
    • A key that is missing takes its default value, which usually means the push "works" but does nothing.

Hub calls (client → server)

Method Payload Server
SubscribeToPlayers {PlayerIds: List<int>}: the client's relationships, the players around it, and itself. Sent on every (re)connect and whenever the set changes, always the complete set. Replaces the connection's subscriptions. The connection is put in a presence:{id} group per player (own id skipped), and PresenceBroadcastService pushes those players' presence to those groups. Only presence, never the subject's other pushes. An unreadable payload keeps the current set.
SubscribeTo / UnsubscribeFrom {Channels: List<string>}, always empty in this build logged only

There is no UnsubscribeFromPlayers.

Handler table

These are the handlers the client registers. "Sent" names what DorkNet sends today.

Key Msg Sent by
1 RelationshipChanged {PlayerID, RelationshipType, Muted, Ignored, Favorited}: the recipient's own view, the same row api/relationships/v2/get lists friend request / accept / remove / add / add-with-code (both players); favorite / mute / ignore and their reverses (caller only)
2 MessageReceived {Id, FromPlayerId (Int32), SentTime, Type, Data (string), RoomId, PlayerEventId}, the same row api/messages/v2/get lists DMs and game invites; SystemNotificationService (cheers, friend request Type 4, accepted Type 40, "wants to join" Type 10, coach message Type 100, event invite text)
3 MessageDeleted {Id} not sent
5 RefreshLogin none not sent
6 Logout {Reason}: a number, enum Unknown 0, UserInitiated 1, SessionTakeover 2, ForciblyLoggedOut 3, Banned 4 NotificationService.LogoutAsync: second-session takeover (2), account deleted or password reset (3), bans (4)
20 / ModerationQuitGame none not sent
22 ModerationKick flat {ReportCategory, Duration, GameSessionId, IsHostKick, Message, PlayerIdReporter, IsBan, IsVoiceModAutoban, IsWarning, VoteKickReason, TimeoutStartedAt} KickPlayerAsync: admin kick, close instance, in-game instant kick, join timeout
ModerationUnkick same as 22 not sent
23 ModerationKickAttemptFailed {ReportCategory, YesVotes, NoVotes, PlayerIdReported} not sent
25 ServerMaintenance {StartsInMinutes}; 0 cancels the countdown admin Broadcast, when a countdown is given
30 / 31 GiftPackageReceived(Immediate) the full gift package, as api/avatar/v2/gifts lists it (AvatarGiftsController.ToWire) gifts, store gift purchases, free gifts, admin gifts
50 RelationshipsInvalid none (the client refetches) admin "everyone is friends" toggle
60 / 61 / 62 StorefrontBalance Add / Update / Purchase 61 {Balance, CurrencyType, Platform}; 62 {BalanceAddType, Delta, Balance, Platform, CurrencyType} LevelService.GrantCurrencyAsync (61), store purchase (62)
70 / 71 ConsumableMapping Added / Removed {Id, ConsumableItemDesc (string), Count, InitialCount, CreatedAt, ActiveDurationMinutes, IsActive, IsTransferable} transfer (70 to the recipient), consume (always 71, with the remaining Count)
80 / 81 PlayerEvent Created / Updated the full player event, as api/playerevents/v2/{id} returns it invites (80 to each invitee); every edit route and the broadcast setting (81 to attendees)
82 / 84 PlayerEvent Deleted / ResponseDeleted {PlayerEventId} event delete (82 to attendees), RSVP removed (84 to the creator)
83 PlayerEventResponseChanged {PlayerEvent, PlayerEventResponse} RSVP (to the creator)
100 InventionModerationStateChanged none not sent
AccountUpdate / SelfAccountUpdate the public / self account DTOs the account REST routes return every profile edit, including profile image (NotificationService.AccountChanged)
PresenceUpdate {playerId, statusVisibility, deviceClass, vrMovementMode, roomInstance, isOnline, appVersion (string)} PresenceBroadcastService, to friends and subscribers, never to the subject (a frame about the local player moves them)
RoomInstanceUpdate the room instance (RoomInstanceDto) admin force-join / instance pull. It only refreshes the cached instance; the move itself comes from the next heartbeat.
RoomUpdate the full room (FGCPNAACHIK, RoomsController.BuildRoomServerDetails), not the 2020 {Room, Scenes} wrapper admin role / owner changes (everyone in the room plus the changed player), admin force-join
ChatMessageReceived {chatMessageId, chatThreadId, senderPlayerId, timeSent, contents} chat sends, to every member including the sender
PlayerLeftChat the same chat-message shape; only chatThreadId and senderPlayerId (the leaver) are read leaving a group chat
PlayerProgressionLevelUpdate {PlayerId, Level, XP}, as api/players/v2/progression returns it every XP award (LevelService.AwardXpAsync)
CommunityBoardUpdate community board community board edits
ReputationUpdate, RoomCurrency*, KeepsakeInstance*, PhotonAccessToken, GoToFailure, AnnouncementUpdate/Delete, ClubMembershipUpdate, GameConfig.Refresh, PlayerSettings.Refresh, … see the contract notes in recroom-2023-client-api-complete.md not sent

Not registered at all (a push with these keys is dropped): numeric 4, 11, 12, 13, 15, 21, 24 (ModerationRoomBan), 40, 85, 90, 95.

Kicks versus logouts

A ModerationKick only sends the player to their dorm, still signed in.

The kick handler builds the dialog text like this:

  • It always starts from its own "Kicked to Dormroom".
  • It appends "Reason: …" from VoteKickReason only when ReportCategory is 10 and IsHostKick is false. Message is never shown.
  • When GameSessionId is the kicked player's current room instance, it also announces the kick to that room.
  • A Duration above 0 starts a timeout follow-up.

Anything that must sign the player out uses Logout instead: bans, account deletion, password resets and second-session takeover. The ban check then refuses the banned player's next login.

Admin broadcast

POST api/admin/v1/broadcast {Message?, StartsInMinutes?}:

  • Message becomes a coach message (MessageReceived Type 100 from the system account) in every online player's notifications.
  • StartsInMinutes starts the client's own maintenance countdown (25).

ServerMaintenance has no text field. The old {Message} payload was never shown, and it cancelled any running countdown.

Not yet done

  • No offline queue. A push sent while the player has no connection is lost. Messages are stored, so they still appear in api/messages/v2/get. RecFlare keeps the newest 500 per player and flushes them on connect.
  • The 2020 save and restore routes (api/rooms/v4/saveData, …/restoredata, api/rooms/v1/datahistory/restore) still push the 2020 RoomUpdate wrapper and a self PresenceUpdate. The 2023 client never calls them; its save (subrooms/{n}/data) and publish routes push nothing and return the room in the HTTP reply.
  • sendfriendintroductions stores Type 30 text instead of FriendIntroduction (130), and pushes nothing. The format of the 130 Data field is unknown.