mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-27 07:58:45 -07:00
feat: Enhance API endpoints with pagination and new subscription details for players
This commit is contained in:
@@ -72,6 +72,7 @@ public class Mod : MelonMod
|
|||||||
public static string PhotonCloudRegion = "eu";
|
public static string PhotonCloudRegion = "eu";
|
||||||
// Photon Custom Auth injector parked 2026-05-28; see
|
// Photon Custom Auth injector parked 2026-05-28; see
|
||||||
// attic/AuthValuesInjector.cs.attic for the code + restore notes.
|
// attic/AuthValuesInjector.cs.attic for the code + restore notes.
|
||||||
|
public static bool BypassPhotonCustomAuth = false;
|
||||||
public static bool EnableTlsTrustBypass = true;
|
public static bool EnableTlsTrustBypass = true;
|
||||||
// Log-only trace for the stuck first-dorm "Change Username?"
|
// Log-only trace for the stuck first-dorm "Change Username?"
|
||||||
// dialog. When true, RegistrationPatches hooks the dorm
|
// dialog. When true, RegistrationPatches hooks the dorm
|
||||||
@@ -200,6 +201,21 @@ public class Mod : MelonMod
|
|||||||
// proxy uses without compile-error.
|
// proxy uses without compile-error.
|
||||||
TryPatchByName("PUNNetworkManager", "OnCustomAuthenticationFailed",
|
TryPatchByName("PUNNetworkManager", "OnCustomAuthenticationFailed",
|
||||||
prefix: nameof(PhotonPatches.OnCustomAuthenticationFailed_Prefix));
|
prefix: nameof(PhotonPatches.OnCustomAuthenticationFailed_Prefix));
|
||||||
|
TryPatchByName("DFPKLPCMNEK", "OnCustomAuthenticationFailed",
|
||||||
|
prefix: nameof(PhotonPatches.OnCustomAuthenticationFailed_Prefix),
|
||||||
|
logMiss: false);
|
||||||
|
TryPatchByName("LOPBLNLAEKB", "OnCustomAuthenticationFailed",
|
||||||
|
prefix: nameof(PhotonPatches.OnCustomAuthenticationFailed_Prefix),
|
||||||
|
logMiss: false);
|
||||||
|
TryPatchByName("LOPBLNLAEKB", "OnDisconnected",
|
||||||
|
prefix: nameof(PhotonPatches.OnDisconnected_Prefix),
|
||||||
|
logMiss: false);
|
||||||
|
TryPatchByName("OLPEMKMGEHE", "OnDisconnected",
|
||||||
|
prefix: nameof(PhotonPatches.OnDisconnected_Prefix),
|
||||||
|
logMiss: false);
|
||||||
|
TryPatchByName("DFPKLPCMNEK", "OnDisconnected",
|
||||||
|
prefix: nameof(PhotonPatches.OnDisconnected_Prefix),
|
||||||
|
logMiss: false);
|
||||||
TryPatchByName("RecRoom.AntiCheat.EACManager",
|
TryPatchByName("RecRoom.AntiCheat.EACManager",
|
||||||
"GenerateChallengeResponse",
|
"GenerateChallengeResponse",
|
||||||
args: new[] { typeof(string) },
|
args: new[] { typeof(string) },
|
||||||
@@ -713,6 +729,7 @@ public class Mod : MelonMod
|
|||||||
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
||||||
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
||||||
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
||||||
|
if (TryGetConfigValue(r, "BypassPhotonCustomAuth", out v)) Cfg.BypassPhotonCustomAuth = v.GetBoolean();
|
||||||
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||||
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||||
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||||
@@ -2531,6 +2548,10 @@ internal static class PhotonPatches
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var t = __result.GetType();
|
var t = __result.GetType();
|
||||||
|
if (Mod.Cfg.BypassPhotonCustomAuth)
|
||||||
|
{
|
||||||
|
DisableCustomAuthValues(__result, t);
|
||||||
|
}
|
||||||
string Read(string name) => t.GetProperty(name)?.GetValue(__result)?.ToString() ?? "<null>";
|
string Read(string name) => t.GetProperty(name)?.GetValue(__result)?.ToString() ?? "<null>";
|
||||||
var authType = Read("AuthType");
|
var authType = Read("AuthType");
|
||||||
var userId = Read("UserId");
|
var userId = Read("UserId");
|
||||||
@@ -2546,6 +2567,56 @@ internal static class PhotonPatches
|
|||||||
catch (Exception ex) { Mod.Log.Warning($"[photon-diag] LogAuthValues failed: {ex.Message}"); }
|
catch (Exception ex) { Mod.Log.Warning($"[photon-diag] LogAuthValues failed: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DisableCustomAuthValues(object authValues, Type t)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetMember(t, authValues, "AuthType", type => AuthTypeNoneValue(type));
|
||||||
|
SetMember(t, authValues, "AuthGetParameters", type => type == typeof(string) ? string.Empty : null);
|
||||||
|
SetMember(t, authValues, "AuthPostData", _ => null);
|
||||||
|
SetMember(t, authValues, "Token", type => type == typeof(string) ? string.Empty : null);
|
||||||
|
Mod.Log.Msg("[photon-auth] custom auth bypass enabled for this launch");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Mod.Log.Warning($"[photon-auth] custom auth bypass failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? AuthTypeNoneValue(Type type)
|
||||||
|
{
|
||||||
|
if (type.IsEnum) return Enum.ToObject(type, 255);
|
||||||
|
if (type == typeof(byte)) return byte.MaxValue;
|
||||||
|
if (type == typeof(sbyte)) return (sbyte)-1;
|
||||||
|
if (type == typeof(short)) return (short)255;
|
||||||
|
if (type == typeof(ushort)) return (ushort)255;
|
||||||
|
if (type == typeof(int)) return 255;
|
||||||
|
if (type == typeof(uint)) return 255u;
|
||||||
|
if (type == typeof(long)) return 255L;
|
||||||
|
if (type == typeof(ulong)) return 255UL;
|
||||||
|
return Convert.ChangeType(255, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SetMember(Type type, object instance, string name, Func<Type, object?> valueFactory)
|
||||||
|
{
|
||||||
|
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||||
|
var prop = type.GetProperty(name, flags);
|
||||||
|
if (prop is not null && prop.CanWrite)
|
||||||
|
{
|
||||||
|
prop.SetValue(instance, valueFactory(prop.PropertyType));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var field = type.GetField(name, flags);
|
||||||
|
if (field is not null)
|
||||||
|
{
|
||||||
|
field.SetValue(instance, valueFactory(field.FieldType));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Prefix on <c>PUNNetworkManager.OnCustomAuthenticationFailed</c>
|
/// <summary>Prefix on <c>PUNNetworkManager.OnCustomAuthenticationFailed</c>
|
||||||
/// — the watch's UI-error formatter only logs a truncated Unity-
|
/// — the watch's UI-error formatter only logs a truncated Unity-
|
||||||
/// stringified version of the debugMessage. Capturing the raw
|
/// stringified version of the debugMessage. Capturing the raw
|
||||||
@@ -2553,13 +2624,18 @@ internal static class PhotonPatches
|
|||||||
/// Photon error including the URL Photon Cloud actually tried to
|
/// Photon error including the URL Photon Cloud actually tried to
|
||||||
/// call (which is the smoking-gun answer to "is the dashboard URL
|
/// call (which is the smoking-gun answer to "is the dashboard URL
|
||||||
/// saved or is Photon hitting a stale one").</summary>
|
/// saved or is Photon hitting a stale one").</summary>
|
||||||
public static void OnCustomAuthenticationFailed_Prefix(object __0)
|
public static bool OnCustomAuthenticationFailed_Prefix(object __0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var debugMessage = __0?.ToString() ?? "<null>";
|
var debugMessage = __0?.ToString() ?? "<null>";
|
||||||
Mod.Log.Error("[photon-diag] OnCustomAuthenticationFailed FULL MESSAGE:");
|
Mod.Log.Error("[photon-diag] OnCustomAuthenticationFailed FULL MESSAGE:");
|
||||||
Mod.Log.Error(debugMessage);
|
Mod.Log.Error(debugMessage);
|
||||||
|
if (Mod.Cfg.BypassPhotonCustomAuth)
|
||||||
|
{
|
||||||
|
Mod.Log.Warning("[photon-auth] custom auth failure suppressed for this launch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Also dump current AppSettings — if Photon rejected the auth
|
// Also dump current AppSettings — if Photon rejected the auth
|
||||||
// we want to know what the watch's actual final config looked
|
// we want to know what the watch's actual final config looked
|
||||||
// like (post-override).
|
// like (post-override).
|
||||||
@@ -2572,6 +2648,23 @@ internal static class PhotonPatches
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Mod.Log.Warning($"[photon-diag] OnCustomAuthenticationFailed log failed: {ex.Message}"); }
|
catch (Exception ex) { Mod.Log.Warning($"[photon-diag] OnCustomAuthenticationFailed log failed: {ex.Message}"); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool OnDisconnected_Prefix(object __0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cause = __0?.ToString() ?? "<null>";
|
||||||
|
if (Mod.Cfg.BypassPhotonCustomAuth &&
|
||||||
|
cause.IndexOf("CustomAuthenticationFailed", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
Mod.Log.Warning("[photon-auth] CustomAuthenticationFailed disconnect suppressed for this launch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Mod.Log.Warning($"[photon-auth] disconnect bypass check failed: {ex.Message}"); }
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Logs every public field + property on <paramref name="obj"/>
|
/// <summary>Logs every public field + property on <paramref name="obj"/>
|
||||||
|
|||||||
@@ -112,12 +112,19 @@ public class CustomAvatarItemsController(
|
|||||||
|
|
||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> Me()
|
public async Task<IActionResult> Me([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||||
{
|
{
|
||||||
var pid = this.RequireCurrentPlayerId();
|
var pid = this.RequireCurrentPlayerId();
|
||||||
var created = await db.CustomAvatarItems
|
skip = Math.Max(0, skip);
|
||||||
|
take = Math.Clamp(take, 1, 100);
|
||||||
|
|
||||||
|
var createdQuery = db.CustomAvatarItems
|
||||||
.Where(i => i.CreatorPlayerId == pid)
|
.Where(i => i.CreatorPlayerId == pid)
|
||||||
.OrderByDescending(i => i.UpdatedAt)
|
.OrderByDescending(i => i.UpdatedAt);
|
||||||
|
var total = await createdQuery.CountAsync();
|
||||||
|
var created = await createdQuery
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var ownedIds = await db.CustomAvatarItemOwnership
|
var ownedIds = await db.CustomAvatarItemOwnership
|
||||||
.Where(o => o.PlayerId == pid)
|
.Where(o => o.PlayerId == pid)
|
||||||
@@ -132,6 +139,8 @@ public class CustomAvatarItemsController(
|
|||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
|
Results = created.Select(ToWire),
|
||||||
|
TotalResults = total,
|
||||||
Created = created.Select(ToWire),
|
Created = created.Select(ToWire),
|
||||||
Owned = owned.Select(ToWire),
|
Owned = owned.Select(ToWire),
|
||||||
});
|
});
|
||||||
@@ -139,15 +148,26 @@ public class CustomAvatarItemsController(
|
|||||||
|
|
||||||
[HttpGet("owned")]
|
[HttpGet("owned")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> Owned()
|
public async Task<IActionResult> Owned([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||||
{
|
{
|
||||||
var pid = this.RequireCurrentPlayerId();
|
var pid = this.RequireCurrentPlayerId();
|
||||||
var rows = await db.CustomAvatarItemOwnership
|
skip = Math.Max(0, skip);
|
||||||
|
take = Math.Clamp(take, 1, 100);
|
||||||
|
|
||||||
|
var query = db.CustomAvatarItemOwnership
|
||||||
.Where(o => o.PlayerId == pid)
|
.Where(o => o.PlayerId == pid)
|
||||||
.Join(db.CustomAvatarItems, o => o.CustomAvatarItemId, i => i.Id, (o, i) => i)
|
.Join(db.CustomAvatarItems, o => o.CustomAvatarItemId, i => i.Id, (o, i) => i)
|
||||||
.OrderByDescending(i => i.UpdatedAt)
|
.OrderByDescending(i => i.UpdatedAt);
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var rows = await query
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(rows.Select(ToWire));
|
return Ok(new
|
||||||
|
{
|
||||||
|
Results = rows.Select(ToWire),
|
||||||
|
TotalResults = total,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("isCreationAllowedForAccount")]
|
[HttpGet("isCreationAllowedForAccount")]
|
||||||
|
|||||||
@@ -1,31 +1,14 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using DorkNet.Server.Auth;
|
|
||||||
using DorkNet.Server.Data;
|
|
||||||
using DorkNet.Server.Data.Entities;
|
|
||||||
|
|
||||||
namespace DorkNet.Server.Controllers.API.GameSight;
|
namespace DorkNet.Server.Controllers.API.GameSight;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
|
||||||
public class GameSightController(DorkNetDbContext db) : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpPost("api/gamesight/event")]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Event([FromBody] object? body)
|
public class GameSightController : ControllerBase
|
||||||
{
|
{
|
||||||
var playerId = this.CurrentPlayerId();
|
[HttpPost("/api/gamesight/event")]
|
||||||
if (playerId is null)
|
[HttpPost("/data/event")]
|
||||||
return Ok(new { Success = true });
|
[HttpPost("/data/heartbeat")]
|
||||||
|
public IActionResult Event() => Ok(new { Success = true });
|
||||||
db.ObjectiveProgress.Add(new ObjectiveProgressEntity
|
|
||||||
{
|
|
||||||
PlayerId = playerId.Value,
|
|
||||||
Key = $"gamesight:{Guid.NewGuid():N}",
|
|
||||||
IsCompleted = true,
|
|
||||||
ClearedAt = DateTime.UtcNow,
|
|
||||||
});
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
return Ok(new { Success = true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class PlayerStateController(
|
|||||||
// values from PlayerEntity.Level / .XP. Default 1/0 when the
|
// values from PlayerEntity.Level / .XP. Default 1/0 when the
|
||||||
// account hasn't been seen yet (synthetic ids passed in queries).
|
// account hasn't been seen yet (synthetic ids passed in queries).
|
||||||
[HttpGet("api/players/v1/progression/{accountId:long}")]
|
[HttpGet("api/players/v1/progression/{accountId:long}")]
|
||||||
|
[HttpGet("api/players/v2/progression/{accountId:long}")]
|
||||||
public async Task<ActionResult<Progression>> GetProgression(long accountId)
|
public async Task<ActionResult<Progression>> GetProgression(long accountId)
|
||||||
{
|
{
|
||||||
var p = await db.Players
|
var p = await db.Players
|
||||||
@@ -51,12 +52,25 @@ public class PlayerStateController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("api/players/v1/progression/bulk")]
|
[HttpPost("api/players/v1/progression/bulk")]
|
||||||
|
[HttpPost("api/players/v2/progression/bulk")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
public async Task<ActionResult<List<Progression>>> GetProgressionBulk(
|
public async Task<ActionResult<List<Progression>>> GetProgressionBulk(
|
||||||
[FromForm(Name = "Ids")] string? ids)
|
[FromForm(Name = "Ids")] string? ids)
|
||||||
{
|
{
|
||||||
var idList = ParseIds(ids).Select(i => (long)i).ToList();
|
var idList = ParseIds(ids).Select(i => (long)i).ToList();
|
||||||
if (idList.Count == 0) return Ok(new List<Progression>());
|
return Ok(await BuildProgressionBulkAsync(idList));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("api/players/v2/progression/bulk")]
|
||||||
|
public async Task<ActionResult<List<Progression>>> GetProgressionBulkV2Get()
|
||||||
|
{
|
||||||
|
var idList = ParseQueryIds().ToList();
|
||||||
|
return Ok(await BuildProgressionBulkAsync(idList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<Progression>> BuildProgressionBulkAsync(IReadOnlyCollection<long> idList)
|
||||||
|
{
|
||||||
|
if (idList.Count == 0) return new List<Progression>();
|
||||||
var rows = await db.Players
|
var rows = await db.Players
|
||||||
.Where(p => idList.Contains(p.Id))
|
.Where(p => idList.Contains(p.Id))
|
||||||
.Select(p => new Progression
|
.Select(p => new Progression
|
||||||
@@ -72,26 +86,40 @@ public class PlayerStateController(
|
|||||||
rows.AddRange(idList
|
rows.AddRange(idList
|
||||||
.Where(id => !found.Contains((int)id))
|
.Where(id => !found.Contains((int)id))
|
||||||
.Select(id => new Progression { PlayerId = (int)id }));
|
.Select(id => new Progression { PlayerId = (int)id }));
|
||||||
return Ok(rows);
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecNet.Reputations.GetReputationById — aggregates CheerEntity
|
// RecNet.Reputations.GetReputationById — aggregates CheerEntity
|
||||||
// by category. Each Cheer* count is the sum of distinct cheers
|
// by category. Each Cheer* count is the sum of distinct cheers
|
||||||
// received from other players in that category.
|
// received from other players in that category.
|
||||||
[HttpGet("api/playerReputation/v1/{accountId:long}")]
|
[HttpGet("api/playerReputation/v1/{accountId:long}")]
|
||||||
|
[HttpGet("api/playerReputation/v2/{accountId:long}")]
|
||||||
public async Task<ActionResult<Reputation>> GetReputation(long accountId) =>
|
public async Task<ActionResult<Reputation>> GetReputation(long accountId) =>
|
||||||
Ok(await BuildReputationAsync(accountId));
|
Ok(await BuildReputationAsync(accountId));
|
||||||
|
|
||||||
[HttpPost("api/playerReputation/v1/bulk")]
|
[HttpPost("api/playerReputation/v1/bulk")]
|
||||||
|
[HttpPost("api/playerReputation/v2/bulk")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
public async Task<ActionResult<List<Reputation>>> GetReputationBulk(
|
public async Task<ActionResult<List<Reputation>>> GetReputationBulk(
|
||||||
[FromForm(Name = "Ids")] string? ids)
|
[FromForm(Name = "Ids")] string? ids)
|
||||||
{
|
{
|
||||||
var idList = ParseIds(ids).Select(i => (long)i).ToList();
|
var idList = ParseIds(ids).Select(i => (long)i).ToList();
|
||||||
|
return Ok(await BuildReputationBulkAsync(idList));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("api/playerReputation/v2/bulk")]
|
||||||
|
public async Task<ActionResult<List<Reputation>>> GetReputationBulkV2Get()
|
||||||
|
{
|
||||||
|
var idList = ParseQueryIds().ToList();
|
||||||
|
return Ok(await BuildReputationBulkAsync(idList));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<Reputation>> BuildReputationBulkAsync(IReadOnlyCollection<long> idList)
|
||||||
|
{
|
||||||
var result = new List<Reputation>(idList.Count);
|
var result = new List<Reputation>(idList.Count);
|
||||||
foreach (var id in idList)
|
foreach (var id in idList)
|
||||||
result.Add(await BuildReputationAsync(id));
|
result.Add(await BuildReputationAsync(id));
|
||||||
return Ok(result);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>POST api/playerReputation/v1/cheer/{playerId} — cheer
|
/// <summary>POST api/playerReputation/v1/cheer/{playerId} — cheer
|
||||||
@@ -216,6 +244,23 @@ public class PlayerStateController(
|
|||||||
.Select(s => int.TryParse(s, out var v) ? v : 0)
|
.Select(s => int.TryParse(s, out var v) ? v : 0)
|
||||||
.Where(v => v != 0);
|
.Where(v => v != 0);
|
||||||
|
|
||||||
|
private IEnumerable<long> ParseQueryIds()
|
||||||
|
{
|
||||||
|
foreach (var (key, values) in Request.Query)
|
||||||
|
{
|
||||||
|
if (long.TryParse(key, out var keyId) && keyId > 0)
|
||||||
|
yield return keyId;
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (long.TryParse(value, out var valueId) && valueId > 0)
|
||||||
|
yield return valueId;
|
||||||
|
foreach (var part in value.ToString().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
if (long.TryParse(part, out var partId) && partId > 0)
|
||||||
|
yield return partId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RecNet.PlayerReporting.GetModerationBlockDetails
|
// RecNet.PlayerReporting.GetModerationBlockDetails
|
||||||
// IMPORTANT: returns a single object, not an array. Defaults indicate
|
// IMPORTANT: returns a single object, not an array. Defaults indicate
|
||||||
// "user is not currently blocked / kicked".
|
// "user is not currently blocked / kicked".
|
||||||
|
|||||||
@@ -187,17 +187,31 @@ public class RoomsController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// `Rooms.GetById(roomId)` — same shape as ByName.
|
/// `Rooms.GetById(roomId)` — same flat shape as ByName.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("api/rooms/v2/{roomId:long}")]
|
[HttpGet("api/rooms/v2/{roomId:long}")]
|
||||||
[HttpGet("api/rooms/v3/{roomId:long}")]
|
[HttpGet("api/rooms/v3/{roomId:long}")]
|
||||||
[HttpGet("rooms/{roomId:long}")]
|
|
||||||
public async Task<IActionResult> ById(long roomId)
|
public async Task<IActionResult> ById(long roomId)
|
||||||
{
|
{
|
||||||
var r = await rooms.GetByIdAsync(roomId);
|
var r = await rooms.GetByIdAsync(roomId);
|
||||||
return Ok(RoomService.ToWireRoom(r ?? Synthetic($"Room_{roomId}", roomId)));
|
return Ok(RoomService.ToWireRoom(r ?? Synthetic($"Room_{roomId}", roomId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("rooms/{roomId:long}")]
|
||||||
|
public async Task<IActionResult> RoomServerById(long roomId)
|
||||||
|
{
|
||||||
|
var room = await rooms.GetByIdAsync(roomId) ?? Synthetic($"Room_{roomId}", roomId);
|
||||||
|
var sceneRows = await db.RoomScenes
|
||||||
|
.Where(s => s.RoomId == room.Id)
|
||||||
|
.OrderBy(s => s.OrderIndex)
|
||||||
|
.ToListAsync();
|
||||||
|
var roles = await db.RoomRoles
|
||||||
|
.Where(r => r.RoomId == room.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(BuildRoomServerDetails(room, sceneRows, roles: roles));
|
||||||
|
}
|
||||||
|
|
||||||
// ── RoomDetails (the boot-critical one) ─────────────────────────────
|
// ── RoomDetails (the boot-critical one) ─────────────────────────────
|
||||||
//
|
//
|
||||||
// /v4/details/{roomId} is hit *during* the matchmaking goto flow.
|
// /v4/details/{roomId} is hit *during* the matchmaking goto flow.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using DorkNet.Models.Auth;
|
using DorkNet.Models.Auth;
|
||||||
@@ -32,9 +33,14 @@ public class AuthController(
|
|||||||
await level.AwardXpAsync(playerId, LevelService.FirstLoginXp, "first_login_today");
|
await level.AwardXpAsync(playerId, LevelService.FirstLoginXp, "first_login_today");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, object> BuildTokenResponse(string accessToken, string refreshToken) => new()
|
private static Dictionary<string, object> BuildTokenResponse(string accessToken, string refreshToken)
|
||||||
|
{
|
||||||
|
var signingKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||||
|
|
||||||
|
return new()
|
||||||
{
|
{
|
||||||
["access_token"] = accessToken,
|
["access_token"] = accessToken,
|
||||||
|
["Access_token"] = accessToken,
|
||||||
["accessToken"] = accessToken,
|
["accessToken"] = accessToken,
|
||||||
["AccessToken"] = accessToken,
|
["AccessToken"] = accessToken,
|
||||||
["token"] = accessToken,
|
["token"] = accessToken,
|
||||||
@@ -46,10 +52,14 @@ public class AuthController(
|
|||||||
["expiresIn"] = 43200,
|
["expiresIn"] = 43200,
|
||||||
["ExpiresIn"] = 43200,
|
["ExpiresIn"] = 43200,
|
||||||
["refresh_token"] = refreshToken,
|
["refresh_token"] = refreshToken,
|
||||||
|
["Refresh_token"] = refreshToken,
|
||||||
["refreshToken"] = refreshToken,
|
["refreshToken"] = refreshToken,
|
||||||
["RefreshToken"] = refreshToken,
|
["RefreshToken"] = refreshToken,
|
||||||
|
["key"] = signingKey,
|
||||||
|
["Key"] = signingKey,
|
||||||
["scope"] = "openid profile",
|
["scope"] = "openid profile",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private IActionResult TokenOk(string accessToken, string refreshToken)
|
private IActionResult TokenOk(string accessToken, string refreshToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -191,6 +191,23 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
|||||||
return Ok(playerSubs + clubSubs);
|
return Ok(playerSubs + clubSubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/subscription/details/{accountId:long}")]
|
||||||
|
public async Task<IActionResult> SubscriptionDetails(long accountId)
|
||||||
|
{
|
||||||
|
var viewerId = this.CurrentPlayerId();
|
||||||
|
var subscriberCount = await clubs.PlayerSubscriberCountAsync(accountId);
|
||||||
|
var subscribedCount = await clubs.PlayerSubscribedCountAsync(accountId);
|
||||||
|
var isSubscribed = viewerId is long viewer
|
||||||
|
&& await clubs.IsSubscribedToPlayerAsync(viewer, accountId);
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
accountId,
|
||||||
|
subscriberCount,
|
||||||
|
subscribedCount,
|
||||||
|
isSubscribed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// POST endpoint the watch hits when the player taps "mark read".
|
/// POST endpoint the watch hits when the player taps "mark read".
|
||||||
/// Idempotent — the upsert in the service quietly returns if a
|
/// Idempotent — the upsert in the service quietly returns if a
|
||||||
|
|||||||
@@ -74,15 +74,38 @@ public class EconController(DorkNetDbContext db, LevelService level) : Controlle
|
|||||||
|
|
||||||
[HttpGet("/econ/customAvatarItems")]
|
[HttpGet("/econ/customAvatarItems")]
|
||||||
[HttpGet("/econ/customAvatarItems/v1/owned")]
|
[HttpGet("/econ/customAvatarItems/v1/owned")]
|
||||||
public async Task<IActionResult> GetOwnedCustomAvatarItems()
|
public async Task<IActionResult> GetOwnedCustomAvatarItems([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||||
{
|
{
|
||||||
var pid = this.RequireCurrentPlayerId();
|
var pid = this.RequireCurrentPlayerId();
|
||||||
var rows = await db.CustomAvatarItemOwnership
|
skip = Math.Max(0, skip);
|
||||||
|
take = Math.Clamp(take, 1, 100);
|
||||||
|
|
||||||
|
var query = db.CustomAvatarItemOwnership
|
||||||
.Where(o => o.PlayerId == pid)
|
.Where(o => o.PlayerId == pid)
|
||||||
.Join(db.CustomAvatarItems, o => o.CustomAvatarItemId, i => i.Id, (o, i) => i)
|
.Join(db.CustomAvatarItems, o => o.CustomAvatarItemId, i => i.Id, (o, i) => i)
|
||||||
.OrderByDescending(i => i.UpdatedAt)
|
.OrderByDescending(i => i.UpdatedAt);
|
||||||
|
|
||||||
|
var total = await query.CountAsync();
|
||||||
|
var rows = await query
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(rows.Select(i => new
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
Results = rows.Select(ToCustomAvatarItemWire),
|
||||||
|
TotalResults = total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/econ/customAvatarItems/v1/itemOwnershipLimit")]
|
||||||
|
public IActionResult CustomAvatarItemOwnershipLimit() => Ok(new
|
||||||
|
{
|
||||||
|
Limit = 1000,
|
||||||
|
ItemOwnershipLimit = 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
private static object ToCustomAvatarItemWire(DorkNet.Server.Data.Entities.CustomAvatarItemEntity i) => new
|
||||||
{
|
{
|
||||||
CustomAvatarItemId = i.PublicId,
|
CustomAvatarItemId = i.PublicId,
|
||||||
Id = i.PublicId,
|
Id = i.PublicId,
|
||||||
@@ -94,15 +117,7 @@ public class EconController(DorkNetDbContext db, LevelService level) : Controlle
|
|||||||
i.ImageName,
|
i.ImageName,
|
||||||
i.AssetName,
|
i.AssetName,
|
||||||
i.Color,
|
i.Color,
|
||||||
}));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("/econ/customAvatarItems/v1/itemOwnershipLimit")]
|
|
||||||
public IActionResult CustomAvatarItemOwnershipLimit() => Ok(new
|
|
||||||
{
|
|
||||||
Limit = 1000,
|
|
||||||
ItemOwnershipLimit = 1000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TokenBalance
|
public class TokenBalance
|
||||||
|
|||||||
@@ -448,5 +448,5 @@ public class PresenceDto
|
|||||||
public bool IsOnline { get; set; } = true;
|
public bool IsOnline { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("appVersion")]
|
[JsonPropertyName("appVersion")]
|
||||||
public int AppVersion { get; set; } = 20201210;
|
public string AppVersion { get; set; } = "20201210";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,5 +179,5 @@ public class PlayerPresenceDto
|
|||||||
public bool IsOnline { get; set; } = true;
|
public bool IsOnline { get; set; } = true;
|
||||||
|
|
||||||
[JsonPropertyName("appVersion")]
|
[JsonPropertyName("appVersion")]
|
||||||
public int AppVersion { get; set; } = 20201210;
|
public string AppVersion { get; set; } = "20201210";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using DorkNet.Server.Auth;
|
using DorkNet.Server.Auth;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
using DorkNet.Server.Data.Entities;
|
using DorkNet.Server.Data.Entities;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace DorkNet.Server.Controllers.PlayerSettings;
|
namespace DorkNet.Server.Controllers.PlayerSettings;
|
||||||
@@ -17,6 +18,30 @@ public class PlayerSettingsController(DorkNetDbContext db) : ControllerBase
|
|||||||
{
|
{
|
||||||
private long CurrentPlayerId => this.RequireCurrentPlayerId();
|
private long CurrentPlayerId => this.RequireCurrentPlayerId();
|
||||||
|
|
||||||
|
[HttpGet("/playersettings")]
|
||||||
|
public async Task<ActionResult<Dictionary<string, string>>> GetMine()
|
||||||
|
{
|
||||||
|
var accountId = CurrentPlayerId;
|
||||||
|
var settings = await db.PlayerSettings
|
||||||
|
.Where(s => s.PlayerId == accountId)
|
||||||
|
.ToDictionaryAsync(s => s.Key, s => s.Value);
|
||||||
|
return Ok(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("/playersettings")]
|
||||||
|
public async Task<IActionResult> SetMine()
|
||||||
|
{
|
||||||
|
var (key, value) = await ReadSettingKeyValueAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return BadRequest(new PlayerSettingsResult { Success = false, Error = "missing_key" });
|
||||||
|
|
||||||
|
key = key.Trim();
|
||||||
|
value ??= string.Empty;
|
||||||
|
|
||||||
|
await UpsertSettingAsync(CurrentPlayerId, key, value);
|
||||||
|
return Ok(new SettingDto { Key = key, Value = value });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("/settings/v1/{accountId:long}")]
|
[HttpGet("/settings/v1/{accountId:long}")]
|
||||||
public async Task<ActionResult<Dictionary<string, string>>> Get(long accountId)
|
public async Task<ActionResult<Dictionary<string, string>>> Get(long accountId)
|
||||||
{
|
{
|
||||||
@@ -36,14 +61,7 @@ public class PlayerSettingsController(DorkNetDbContext db) : ControllerBase
|
|||||||
if (accountId != CurrentPlayerId) return Forbid();
|
if (accountId != CurrentPlayerId) return Forbid();
|
||||||
|
|
||||||
foreach (var (key, value) in updates)
|
foreach (var (key, value) in updates)
|
||||||
{
|
await UpsertSettingAsync(accountId, key, value, save: false);
|
||||||
var existing = await db.PlayerSettings
|
|
||||||
.FirstOrDefaultAsync(s => s.PlayerId == accountId && s.Key == key);
|
|
||||||
if (existing is null)
|
|
||||||
db.PlayerSettings.Add(new PlayerSettingEntity { PlayerId = accountId, Key = key, Value = value });
|
|
||||||
else
|
|
||||||
existing.Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -62,4 +80,90 @@ public class PlayerSettingsController(DorkNetDbContext db) : ControllerBase
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpsertSettingAsync(long accountId, string key, string value, bool save = true)
|
||||||
|
{
|
||||||
|
var existing = await db.PlayerSettings
|
||||||
|
.FirstOrDefaultAsync(s => s.PlayerId == accountId && s.Key == key);
|
||||||
|
if (existing is null)
|
||||||
|
db.PlayerSettings.Add(new PlayerSettingEntity { PlayerId = accountId, Key = key, Value = value });
|
||||||
|
else
|
||||||
|
existing.Value = value;
|
||||||
|
|
||||||
|
if (save) await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string? Key, string? Value)> ReadSettingKeyValueAsync()
|
||||||
|
{
|
||||||
|
string? key = FirstValue(Request.Query, "key", "Key");
|
||||||
|
string? value = FirstValue(Request.Query, "value", "Value");
|
||||||
|
|
||||||
|
if (Request.HasFormContentType)
|
||||||
|
{
|
||||||
|
var form = await Request.ReadFormAsync();
|
||||||
|
key ??= FirstValue(form, "key", "Key");
|
||||||
|
value ??= FirstValue(form, "value", "Value");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is null && Request.ContentLength.GetValueOrDefault() > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = await JsonDocument.ParseAsync(Request.Body);
|
||||||
|
if (document.RootElement.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
key = FirstJsonString(document.RootElement, "key", "Key");
|
||||||
|
value = FirstJsonString(document.RootElement, "value", "Value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Non-JSON bodies are handled by form/query parsing above.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstValue(IQueryCollection values, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
if (values.TryGetValue(name, out var value))
|
||||||
|
return value.ToString();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstValue(IFormCollection values, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
if (values.TryGetValue(name, out var value))
|
||||||
|
return value.ToString();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstJsonString(JsonElement obj, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
if (obj.TryGetProperty(name, out var prop))
|
||||||
|
return prop.ValueKind == JsonValueKind.String ? prop.GetString() : prop.ToString();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SettingDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Key")]
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("Value")]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerSettingsResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("Error")]
|
||||||
|
public string Error { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,15 @@ public class ClubService(DorkNetDbContext db)
|
|||||||
public Task<int> PlayerSubscriberCountAsync(long playerId) =>
|
public Task<int> PlayerSubscriberCountAsync(long playerId) =>
|
||||||
db.Subscriptions.CountAsync(s => s.TargetPlayerId == playerId);
|
db.Subscriptions.CountAsync(s => s.TargetPlayerId == playerId);
|
||||||
|
|
||||||
|
/// <summary>How many player/profile subscriptions this account has
|
||||||
|
/// made.</summary>
|
||||||
|
public Task<int> PlayerSubscribedCountAsync(long playerId) =>
|
||||||
|
db.Subscriptions.CountAsync(s => s.SubscriberPlayerId == playerId);
|
||||||
|
|
||||||
|
public Task<bool> IsSubscribedToPlayerAsync(long subscriberId, long targetPlayerId) =>
|
||||||
|
db.Subscriptions.AnyAsync(s => s.SubscriberPlayerId == subscriberId
|
||||||
|
&& s.TargetPlayerId == targetPlayerId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bulk member-count lookup so the controller can project a list
|
/// Bulk member-count lookup so the controller can project a list
|
||||||
/// of clubs in one round-trip. Returns a dict keyed by ClubId; any
|
/// of clubs in one round-trip. Returns a dict keyed by ClubId; any
|
||||||
|
|||||||
Reference in New Issue
Block a user