mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
6ee1d6f9b3
Every change below was checked against the 2023-03-21 client disassembly;
file:line evidence is in the code comments and in
docs/recroom-2023-client-api-complete.md.
Presence
- matchmake/none means "online, in no room" (its only caller is
TryGoToOfflineRoom, which ignores the reply). The server used to keep the
old room or invent a dorm, so the next heartbeat looked out of sync and
the client cancelled its load to chase that room.
- The join watchdog no longer clears presence 30 s into a long room load;
it is a 3 minute backstop that drops the room only.
- One fan-out (PresenceBroadcastService) for every transition: deferred
joins, matchmake/none, logout, failed joins, visibility changes, SignalR
connect/disconnect. isOnline is sent on its own; pushes never go to the
subject, because a self push moves the player.
- Online-status visibility (Public / Friends / Favorites / Appear offline)
is stored and honoured by GET /player and by the live pushes.
Profile
- PUT account/me/{emoji,personalpronouns,identityflags,bannerimage} were
404; added with the client's real form keys and persisted (new Players
columns plus migration). Every account read now returns them, and edits
push AccountUpdate to everyone.
Outfit menu
- Saved outfits kept OutfitSelectionsV2 and CustomAvatarItems, so loading a
slot restores what was saved instead of a stale outfit.
- isCreationAllowedForAccount answers the {Success, Error} envelope the
client parses; minPriceForPublicItem is a bare int; gifts/generate accepts
form bodies; lockeditems returns the full owned-item shape; colourways no
longer share their base item's AvatarItemId.
RecNetResult writes
- About 40 writes look status-only but parse the body as {Success, Error}.
An empty 200 showed "Failed to ..." after the server applied the change,
and a body without Success threw "Error message cannot be null". Clubs,
announcements, keepsakes, comments/read, subscriptions, purchase offers
and privacy settings now answer RecNetResult.
- Login token and device-code grants are real; remoteauth approves the
device code; createlogintoken returns a bare string.
- New: incentivizedreferrals/referrals, account/bulk/{email,phone},
GET strings, account/me/confirmphone, PlayMenuCarouselOrdering_1.
Route inventory and coverage test extended to match.
README now describes the 2023 branch.
110 lines
4.7 KiB
C#
110 lines
4.7 KiB
C#
using DorkNet.Server.Data;
|
|
using DorkNet.Server.Data.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace DorkNet.Server.Services;
|
|
|
|
/// <summary>
|
|
/// OAuth device-authorization grant as the 2023 client drives it.
|
|
///
|
|
/// 1. The device that wants to sign in calls <c>connect/deviceauthorization</c>
|
|
/// and shows the player the <c>user_code</c> (reader
|
|
/// <c>RecNet.Runtime/AMPFGJPKHOD.txt:399-598</c>: device_code, user_code,
|
|
/// verification_uri(_complete), expires_in, interval).
|
|
/// 2. It polls <c>connect/token</c> with
|
|
/// <c>grant_type=urn:ietf:params:oauth:grant-type:device_code</c> and
|
|
/// <c>device_code</c> (<c>LBNJFPOLCDL_NestedType_DDNJDALGIME.txt:444-451</c>).
|
|
/// A 400 whose <c>error</c> is <c>authorization_pending</c> or
|
|
/// <c>slow_down</c> means "keep polling"
|
|
/// (<c>LBNJFPOLCDL_NestedType_JAEKFNKDNCP.txt:97-116</c>); anything else
|
|
/// ends the attempt.
|
|
/// 3. A signed-in client approves the code with
|
|
/// <c>account/me/remoteauth</c> (form field <c>code</c>).
|
|
///
|
|
/// State lives in one PlayerSettings row per authorization, owned by the system
|
|
/// account: key <c>deviceauthorization:{device_code}</c>, value
|
|
/// <c>{user_code}|{expiresAtUtc:O}</c>, plus <c>|{approverAccountId}</c> once
|
|
/// approved. Redeeming deletes the row, so a device code is single-use.
|
|
/// </summary>
|
|
public static class DeviceAuthorizations
|
|
{
|
|
public const string KeyPrefix = "deviceauthorization:";
|
|
public static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(15);
|
|
|
|
public enum RedeemStatus { Invalid, Expired, Pending, Approved }
|
|
|
|
public sealed record Created(string DeviceCode, string UserCode, int ExpiresInSeconds);
|
|
|
|
public static async Task<Created> CreateAsync(DorkNetDbContext db, DateTime nowUtc)
|
|
{
|
|
var deviceCode = Guid.NewGuid().ToString("N");
|
|
var userCode = deviceCode[..8].ToUpperInvariant();
|
|
db.PlayerSettings.Add(new PlayerSettingEntity
|
|
{
|
|
PlayerId = RelationshipQueries.SystemAccountId,
|
|
Key = KeyPrefix + deviceCode,
|
|
Value = $"{userCode}|{(nowUtc + Lifetime):O}",
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return new Created(deviceCode, userCode, (int)Lifetime.TotalSeconds);
|
|
}
|
|
|
|
/// <summary>Bind the pending authorization whose user code is
|
|
/// <paramref name="userCode"/> to <paramref name="approverId"/>. False when
|
|
/// no live, unapproved authorization carries that code.</summary>
|
|
public static async Task<bool> ApproveAsync(
|
|
DorkNetDbContext db, string userCode, long approverId, DateTime nowUtc)
|
|
{
|
|
var code = NormaliseUserCode(userCode);
|
|
if (code.Length == 0) return false;
|
|
|
|
var prefix = code + "|";
|
|
var rows = await db.PlayerSettings
|
|
.Where(s => s.Key.StartsWith(KeyPrefix) && s.Value.StartsWith(prefix))
|
|
.ToListAsync();
|
|
foreach (var row in rows)
|
|
{
|
|
var parts = row.Value.Split('|');
|
|
if (parts.Length < 2 || !TryParseExpiry(parts[1], out var expires) || expires < nowUtc)
|
|
continue;
|
|
if (parts.Length >= 3 && parts[2].Length > 0) continue; // already approved
|
|
row.Value = $"{parts[0]}|{parts[1]}|{approverId}";
|
|
await db.SaveChangesAsync();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>One poll from the device. Approved authorizations are consumed
|
|
/// and yield the approving account.</summary>
|
|
public static async Task<(RedeemStatus Status, long AccountId)> RedeemAsync(
|
|
DorkNetDbContext db, string? deviceCode, DateTime nowUtc)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(deviceCode)) return (RedeemStatus.Invalid, 0);
|
|
var row = await db.PlayerSettings
|
|
.FirstOrDefaultAsync(s => s.Key == KeyPrefix + deviceCode.Trim());
|
|
if (row is null) return (RedeemStatus.Invalid, 0);
|
|
|
|
var parts = row.Value.Split('|');
|
|
if (parts.Length < 2 || !TryParseExpiry(parts[1], out var expires) || expires < nowUtc)
|
|
{
|
|
db.PlayerSettings.Remove(row);
|
|
await db.SaveChangesAsync();
|
|
return (RedeemStatus.Expired, 0);
|
|
}
|
|
|
|
if (parts.Length < 3 || !long.TryParse(parts[2], out var approver))
|
|
return (RedeemStatus.Pending, 0);
|
|
|
|
db.PlayerSettings.Remove(row);
|
|
await db.SaveChangesAsync();
|
|
return (RedeemStatus.Approved, approver);
|
|
}
|
|
|
|
private static string NormaliseUserCode(string? raw) =>
|
|
new string((raw ?? string.Empty).Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
|
|
|
|
private static bool TryParseExpiry(string raw, out DateTime expiresUtc) =>
|
|
DateTime.TryParse(raw, null, System.Globalization.DateTimeStyles.RoundtripKind, out expiresUtc);
|
|
}
|