feat: Add support for 2018 client API endpoints and data models

- Implemented new search endpoints for rooms to accommodate the 2018 client.
- Added support for retrieving room details and featured rooms in a format compatible with the 2018 client.
- Introduced new data models for login responses and player presence specific to the 2018 client.
- Created mappers to convert between internal entities and the 2018 wire formats for players and rooms.
- Updated existing storefront APIs to include versioned endpoints for the 2018 client.
- Enhanced the version controller to return dual-format responses for compatibility with both 2018 and 2020 clients.
- Refactored the configuration service to ensure trailing slashes in URLs for the 2018 client.
This commit is contained in:
Alexa
2026-06-07 15:32:52 +03:00
parent 955246c6e4
commit 579071858a
21 changed files with 770 additions and 94 deletions
+118
View File
@@ -0,0 +1,118 @@
using System.Text.Json.Serialization;
namespace DorkNet.Models.Auth;
// ── June-2018 (build 20180621_EA) login wire shapes ──────────────────────────
//
// The 2018 client speaks a DIFFERENT login protocol than the 2019/2020 OAuth
// flow the rest of DorkNet implements:
// • POST api/platformlogin/v1/{loginaccount,logincached,createaccount}
// • request body is a JSON object (Unity JsonUtility / LitJson), NOT form
// • response is the "login envelope" below, read by RecNet.cs:40014-40021
// via case-sensitive LitJson keys.
//
// Keys verified from the assembly's REAL (name-preserved) Deserialize methods
// via dnlib — the obfuscated-name sibling methods are Beebyte decoys with
// scrambled literals (see project_2018_mono_obfuscation memory).
/// <summary>
/// Login response envelope. RecNet login-response Deserialize (RecNet.cs:40016-40020)
/// reads: Error (req string), Player (optional object), Token (req string),
/// FirstLoginOfTheDay (req bool), AnalyticsSessionId (req long). Empty Error
/// means success; a non-empty Error is shown to the user and aborts login.
/// </summary>
public class Login2018Response
{
[JsonPropertyName("Error")]
public string Error { get; set; } = string.Empty;
[JsonPropertyName("Player")]
public Player2018? Player { get; set; }
[JsonPropertyName("Token")]
public string Token { get; set; } = string.Empty;
[JsonPropertyName("FirstLoginOfTheDay")]
public bool FirstLoginOfTheDay { get; set; }
[JsonPropertyName("AnalyticsSessionId")]
public long AnalyticsSessionId { get; set; }
}
/// <summary>
/// 2018 player object embedded in the login envelope and returned bare from
/// getcachedlogins. Keys + required/optional verified from RecNet.cs:64632-64667
/// (RecNet.EEBPLECPEGD.Deserialize):
/// REQUIRED (Util.GetKey throws if missing): Id, Username, DisplayName, XP,
/// Level, RegistrationStatus, Developer, CanReceiveInvites, ProfileImageName,
/// JuniorProfile, ForceJuniorImages, PendingJunior, HasBirthday.
/// OPTIONAL: Bio (TryGetKey), AvoidJuniors (ContainsKey-guarded),
/// PlayerReputation/PlatformId (GetObjectKey -> null ok; omitted here),
/// GroupMemberships (ContainsKey-guarded; omitted).
/// NOTE the 2018 client uses "Id" (not "AccountId") and "Developer" (not
/// "IsDeveloper") — distinct from the 2020 PlayerProfile DTO.
/// </summary>
public class Player2018
{
[JsonPropertyName("Id")]
public long Id { get; set; }
[JsonPropertyName("Username")]
public string Username { get; set; } = string.Empty;
[JsonPropertyName("DisplayName")]
public string DisplayName { get; set; } = string.Empty;
[JsonPropertyName("Bio")]
public string Bio { get; set; } = string.Empty;
[JsonPropertyName("XP")]
public int XP { get; set; }
[JsonPropertyName("Level")]
public int Level { get; set; } = 1;
// Obfuscated enum in the dump; not a gate (the client just stores it). 3 is
// the "fully registered" value used elsewhere; verify against a live client
// if the signup flow re-appears for an existing account.
[JsonPropertyName("RegistrationStatus")]
public int RegistrationStatus { get; set; } = 3;
[JsonPropertyName("Developer")]
public bool Developer { get; set; }
[JsonPropertyName("CanReceiveInvites")]
public bool CanReceiveInvites { get; set; } = true;
[JsonPropertyName("ProfileImageName")]
public string ProfileImageName { get; set; } = string.Empty;
[JsonPropertyName("JuniorProfile")]
public bool JuniorProfile { get; set; }
[JsonPropertyName("ForceJuniorImages")]
public bool ForceJuniorImages { get; set; }
[JsonPropertyName("PendingJunior")]
public bool PendingJunior { get; set; }
[JsonPropertyName("HasBirthday")]
public bool HasBirthday { get; set; }
[JsonPropertyName("AvoidJuniors")]
public bool AvoidJuniors { get; set; }
}
/// <summary>
/// Generic 2018 success envelope {Success, Message} (RecNet EFDOILDKBFK) used by
/// registeraccount (email attach) and other simple POSTs. Note the key is
/// "Message" (not "error").
/// </summary>
public class SuccessMessage2018
{
[JsonPropertyName("Success")]
public bool Success { get; set; }
[JsonPropertyName("Message")]
public string Message { get; set; } = string.Empty;
}
+22 -1
View File
@@ -50,13 +50,34 @@ public class RecRoomConfig
[JsonPropertyName("DailyObjectives")]
public List<List<DailyObjective>> DailyObjectives { get; set; } = DailyObjective.DefaultSets();
[JsonPropertyName("ConfigTable")]
// 2018.06 client (RecNet.OPOHNGAOCCD.Deserialize, RecNet.cs:9062-9068) reads
// ConfigTable as an ARRAY of {Key,Value} objects, NOT a JSON object map:
// foreach elem in dict["ConfigTable"]: table[elem["Key"]] = elem["Value"]
// The 2020 client read it as an object — but this branch targets 2018, so we
// emit the array form. Keep the settable Dictionary for existing call sites
// (ConfigService.GetConfig populates it) and project to the wire array.
[JsonIgnore]
public Dictionary<string, string> ConfigTable { get; set; } = [];
[JsonPropertyName("ConfigTable")]
public List<ConfigTableEntry> ConfigTableWire =>
ConfigTable.Select(kv => new ConfigTableEntry { Key = kv.Key, Value = kv.Value }).ToList();
[JsonPropertyName("ServiceUrls")]
public Dictionary<string, string> ServiceUrls { get; set; } = [];
}
// 2018 ConfigTable element shape: {"Key": "...", "Value": "..."} (PascalCase,
// LitJson Util.GetKey<string>("Key"/"Value")).
public class ConfigTableEntry
{
[JsonPropertyName("Key")]
public string Key { get; set; } = string.Empty;
[JsonPropertyName("Value")]
public string Value { get; set; } = string.Empty;
}
public class PhotonConfig
{
[JsonPropertyName("AppId")]
+21
View File
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace DorkNet.Models.Players;
/// <summary>
/// June-2018 presence record returned from POST api/presence/v2/list.
/// Keys verified from RecNet.DBHHFILLFLC.Deserialize: PlayerId (long),
/// IsOnline (bool), GameSession (optional object — null when the player is
/// online but not in a session; the client null-checks it).
/// </summary>
public class Presence2018
{
[JsonPropertyName("PlayerId")]
public long PlayerId { get; set; }
[JsonPropertyName("IsOnline")]
public bool IsOnline { get; set; }
[JsonPropertyName("GameSession")]
public object? GameSession { get; set; }
}
+48
View File
@@ -0,0 +1,48 @@
using System.Text.Json.Serialization;
namespace DorkNet.Models.Rooms;
/// <summary>
/// Flat June-2018 room object. The 2018 client (RecNet Room.Deserialize,
/// RecNet.cs:78244) reads these keys directly off the room JSON — there is NO
/// 2020-style RoomDetails/Scenes wrapper; DataBlobName + PersonalDetails are
/// embedded right here. Required keys throw (Util.GetKey) if missing, so every
/// field below must be present except the genuinely-optional ones noted.
///
/// Notes:
/// • CreatorPlayerId is read as an int client-side; all account ids fit int.
/// • Date keys are ISO-8601 (client DateTimeParse.ParseISO8601). Use real
/// dates, never year-9999 (overflow — see WireDates).
/// • ActivityLevelId parse failure is a non-fatal warning client-side, so ""
/// is safe for non-activity rooms.
/// • CoOwners/Hosts are int arrays (empty ok); PersonalDetails is an optional
/// object (null ok).
/// </summary>
public class Room2018
{
[JsonPropertyName("RoomId")] public long RoomId { get; set; }
[JsonPropertyName("Name")] public string Name { get; set; } = string.Empty;
[JsonPropertyName("Description")] public string Description { get; set; } = string.Empty;
[JsonPropertyName("CreatorPlayerId")] public long CreatorPlayerId { get; set; }
[JsonPropertyName("ImageName")] public string ImageName { get; set; } = string.Empty;
[JsonPropertyName("DataBlobName")] public string DataBlobName { get; set; } = string.Empty;
[JsonPropertyName("ActivityLevelId")] public string ActivityLevelId { get; set; } = string.Empty;
[JsonPropertyName("IsSandbox")] public bool IsSandbox { get; set; }
[JsonPropertyName("Instanced")] public bool Instanced { get; set; } = true;
[JsonPropertyName("MaxPlayers")] public int MaxPlayers { get; set; } = 20;
[JsonPropertyName("Accessibility")] public int Accessibility { get; set; } = 1;
[JsonPropertyName("AccessibilityLocked")] public bool AccessibilityLocked { get; set; }
[JsonPropertyName("VisitorCount")] public int VisitorCount { get; set; }
[JsonPropertyName("VisitCount")] public int VisitCount { get; set; }
[JsonPropertyName("CheerCount")] public int CheerCount { get; set; }
[JsonPropertyName("ReportCount")] public int ReportCount { get; set; }
[JsonPropertyName("State")] public int State { get; set; }
[JsonPropertyName("StateModifiedAt")] public DateTime StateModifiedAt { get; set; }
[JsonPropertyName("CreatedAt")] public DateTime CreatedAt { get; set; }
[JsonPropertyName("ModifiedAt")] public DateTime ModifiedAt { get; set; }
[JsonPropertyName("LastVisitedAt")] public DateTime LastVisitedAt { get; set; }
[JsonPropertyName("DataModifiedAt")] public DateTime DataModifiedAt { get; set; }
[JsonPropertyName("CoOwners")] public List<int> CoOwners { get; set; } = [];
[JsonPropertyName("Hosts")] public List<int> Hosts { get; set; } = [];
[JsonPropertyName("PersonalDetails")] public object? PersonalDetails { get; set; }
}