Files
2026-10-16 23:33:13 -07:00

724 lines
24 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text;
using System.Text.Json.Nodes;
using RadicadeServer.API.Notifications;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Enums;
using RadicadeServer.RecNet.Enums.Rooms;
using RadicadeServer.RecNet.Notifications;
using RadicadeServer.RecNet.Responses;
using RadicadeServer.RecNet.Rooms;
using RadicadeServer.Utils;
namespace RadicadeServer.RoomServer
{
[Route("rooms/rooms/")]
public class V2_RoomsAPI(RecDBContext db) : Controller
{
[HttpGet("{id:long}/similar"), Authorize]
public async Task<IActionResult> GetSimilarRooms(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Ok(EmptyPage());
var userId = User.GetUserId();
skip = Math.Max(skip, 0);
take = Math.Clamp(take, 1, 100);
var ownTags = StringTools.ReadTags(room.Tags)
.Select(t => t.Tag?.Trim().ToLowerInvariant())
.Where(t => !string.IsNullOrEmpty(t))
.ToHashSet();
var candidates = await db.Rooms
.Where(x => x.RoomId != id && x.Accessibility == Accessibility.Public)
.ToListAsync();
var scored = new List<(RoomData Room, int Score, int Visits)>();
foreach (var candidate in candidates)
{
if (!await RoomUtils.CanSeeRoom(candidate, userId)) continue;
int score;
if (ownTags.Count == 0)
{
score = candidate.CreatorAccountId == room.CreatorAccountId ? 1 : 0;
}
else
{
score = StringTools.ReadTags(candidate.Tags)
.Count(t => ownTags.Contains(t.Tag?.Trim().ToLowerInvariant()));
}
if (score <= 0) continue;
var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == candidate.RoomId);
scored.Add((candidate, score, stats?.VisitCount ?? 0));
}
var ordered = scored
.OrderByDescending(x => x.Score)
.ThenByDescending(x => x.Visits)
.Select(x => x.Room)
.ToList();
var picked = ordered.Skip(skip).Take(take).ToList();
var details = await RoomUtils.BulkConvertRoomDataToRoomDetail(picked);
return Ok(new RoomSearchResult { TotalResults = ordered.Count, Results = details });
}
private static RoomSearchResult EmptyPage() => new() { TotalResults = 0, Results = new List<RoomDetail>() };
[HttpGet("{id:long}/experience")]
public IActionResult GetRoomExperience(long id)
=> Ok(new { Enabled = false, DailyLimit = 1000 });
[HttpGet("{id:long}/experience/player")]
public IActionResult GetRoomExperienceForPlayer(long id)
=> Ok(Array.Empty<object>());
[HttpPut("{id}/description"), Authorize]
public async Task<Success> SetDescription(long? id, string description)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
if (!description.IsPure()) return new Success(false, "Description contains offensive language.", null);
room.Description = description;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/name"), Authorize]
public async Task<Success> SetName(long? id, string name)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null);
var sameRoomWithName = await db.Rooms.FirstOrDefaultAsync(x => x.Name == name);
if (sameRoomWithName != null) return new Success(false, "A room already exists with that name.", null);
room.Name = name;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/warning"), Authorize]
public async Task<Success> SetWarning(long? id, WarningMask? warningMask, string? customWarning)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
if (warningMask != null)
room.WarningMask = warningMask.Value;
if (customWarning != null)
room.CustomWarning = customWarning;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/cloning"), Authorize]
public async Task<Success> SetAllowedCloning(long? id, bool cloningAllowed)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
room.CloningAllowed = cloningAllowed;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/restrictions"), Authorize]
public async Task<Success> SetRestrictions(long? id, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
room.SupportsTeleportVR = supportsTeleportVR;
room.SupportsWalkVR = supportsWalkVR;
room.SupportsScreens = supportsScreens;
room.SupportsJuniors = supportsJuniors;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/automute"), Authorize]
public async Task<Success> SetDisableAutoMute(long? id, bool disable)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
room.DisableMicAutoMute = disable;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/comments"), Authorize]
public async Task<Success> SetDisableComments(long? id, bool disable)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
room.DisableRoomComments = disable;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/voice_chat_encryption"), Authorize]
public async Task<Success> SetEncryptVoiceChat(long? id, bool encryptVoiceChat)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
room.EncryptVoiceChat = encryptVoiceChat;
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpPut("{id}/allow_new_users"), Authorize]
public async Task<Success> SetAllowNewUsers(long? id, bool allowNewUsers)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
[HttpGet("{id}/bans"), Authorize]
public async Task<List<RoomBan>> GetRoomBans(long? id)
{
var userId = User.GetUserId();
if (userId == null || id == null) return new List<RoomBan>();
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return new List<RoomBan>();
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
if (!canModify) return new List<RoomBan>();
return await db.RoomBans
.Where(x => x.roomId == id && (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow))
.OrderByDescending(x => x.bannedAt)
.ToListAsync();
}
[HttpGet("{id}/bans/{bannedPlayerId}"), Authorize]
public async Task<RoomBan?> GetRoomBan(long? id, long bannedPlayerId)
{
var userId = User.GetUserId();
if (userId == null || id == null) return null;
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return null;
if (bannedPlayerId != (long)userId)
{
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
if (!canModify) return null;
}
return await db.RoomBans
.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == bannedPlayerId &&
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
}
[HttpPost("{id}/bans/import"), Authorize]
public async Task<Success> ImportRoomBans(long? id)
{
var userId = User.GetUserId();
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
if (!canModify) return Success.Error("Insufficient permissions");
var playerIds = new List<long>();
string rawBody = "";
try
{
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players" })
if (form.TryGetValue(key, out var values))
foreach (var value in values)
foreach (var part in (value ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
if (long.TryParse(part, out var parsedId) && parsedId > 0)
playerIds.Add(parsedId);
}
if (Request.ContentLength is > 0)
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
rawBody = await reader.ReadToEndAsync();
if (!string.IsNullOrWhiteSpace(rawBody))
{
var parsedBody = JsonNode.Parse(rawBody);
if (parsedBody is JsonArray array)
{
foreach (var item in array)
{
if (item is JsonValue value && value.TryGetValue<long>(out var bareId) && bareId > 0)
playerIds.Add(bareId);
else if (item is JsonObject obj)
playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" }));
}
}
else if (parsedBody is JsonObject body)
{
foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players", "bans", "Bans" })
{
if (!body.TryGetPropertyValue(key, out var node) || node == null) continue;
if (node is JsonArray arr)
{
foreach (var item in arr)
{
if (item is JsonValue value && value.TryGetValue<long>(out var bareId) && bareId > 0)
playerIds.Add(bareId);
else if (item is JsonObject obj)
playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" }));
}
}
}
}
}
}
}
catch { }
playerIds = playerIds.Distinct().ToList();
if (playerIds.Count == 0)
return Success.Error("No valid player ids found in the import request");
var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId);
var imported = new List<RoomBan>();
var now = DateTime.UtcNow;
foreach (var targetPlayer in playerIds)
{
if (targetPlayer == (long)userId) continue;
if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner)) continue;
if (await db.Profiles.AnyAsync(x => x.accountId == targetPlayer) == false) continue;
var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer &&
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
if (existing != null) continue;
var ban = new RoomBan
{
roomId = (long)id!,
actorId = (long)userId,
bannedPlayerId = targetPlayer,
bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}",
reason = "",
bannedAt = now,
expiresAt = null
};
db.RoomBans.Add(ban);
imported.Add(ban);
}
await db.SaveChangesAsync();
return new Success(true, null, new Dictionary<string, object?>
{
["imported"] = imported.Count,
["importedCount"] = imported.Count,
["requested"] = playerIds.Count,
["bans"] = imported,
["playerIds"] = imported.Select(x => x.bannedPlayerId).ToArray()
});
}
[HttpPut("{id}/bans"), Authorize]
[HttpPost("{id}/bans"), Authorize]
public async Task<Success> AddRoomBan(long? id)
{
var userId = User.GetUserId();
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
if (!canModify)
{
var deniedRole = await RoomUtils.GetRoleForPlayer((long)userId, (long)id!);
var highRanksVal = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks");
return Success.Error("Insufficient permissions");
}
long targetPlayer = 0;
string banReason = "";
string[] idKeys =
{
"bannedPlayerId", "bannedPlayerID", "bannedPlayer", "bannedplayerid",
"bannedId", "bannedid", "banned_id",
"bannerId", "bannerid", "banner_id",
"playerId", "playerid", "player_id", "player", "playerAccountId",
"bannedUserId", "bannedAccountId",
"accountId", "account_id",
"targetPlayerId", "targetPlayer", "targetId", "targetid", "target",
"victimId", "victimid",
"userId", "userid",
"id"
};
if (Request.HasFormContentType)
{
foreach (var key in idKeys)
if (targetPlayer <= 0 && long.TryParse(Request.Form[key], out var formPlayer)) targetPlayer = formPlayer;
if (string.IsNullOrEmpty(banReason))
foreach (var key in new[] { "reason", "banReason", "ban_reason" })
if (Request.Form.TryGetValue(key, out var formReason)) { banReason = formReason.ToString(); break; }
}
foreach (var key in idKeys)
if (targetPlayer <= 0 && long.TryParse(Request.Query[key], out var queryPlayer)) targetPlayer = queryPlayer;
if (string.IsNullOrEmpty(banReason))
foreach (var key in new[] { "reason", "banReason", "ban_reason" })
if (Request.Query.TryGetValue(key, out var queryReason)) { banReason = queryReason.ToString(); break; }
string rawBody = "";
try
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
rawBody = await reader.ReadToEndAsync();
if (!string.IsNullOrWhiteSpace(rawBody))
{
var parsedBody = JsonNode.Parse(rawBody);
if (parsedBody is JsonObject body)
{
if (targetPlayer <= 0) targetPlayer = ParsePlayerId(body, idKeys);
if (string.IsNullOrEmpty(banReason)) banReason = ParseJsonString(body, "reason", "banReason", "ban_reason");
}
else if (parsedBody is JsonValue bareValue && targetPlayer <= 0)
{
if (bareValue.TryGetValue<long>(out var bareId)) targetPlayer = bareId;
else if (bareValue.TryGetValue<string>(out var bareStr) && long.TryParse(bareStr, out var bareParsed)) targetPlayer = bareParsed;
}
}
}
catch { }
if (targetPlayer <= 0)
{
return Success.Error("bannedPlayerId is required");
}
if (targetPlayer == (long)userId)
{
return Success.Error("You cannot ban yourself");
}
if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner))
return Success.Error("You cannot ban a Co-Owner or higher");
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == targetPlayer);
if (profile == null) return Success.Error("Player not found");
var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer &&
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
if (existing != null) return Success.Error("Player is already banned from this room");
var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId);
var ban = new RoomBan
{
roomId = (long)id!,
actorId = (long)userId,
bannedPlayerId = targetPlayer,
bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}",
reason = banReason.Trim(),
bannedAt = DateTime.UtcNow,
expiresAt = null
};
await db.RoomBans.AddAsync(ban);
await db.SaveChangesAsync();
await MatchmakingUtils.KickPlayerFromRoom(targetPlayer, (long)id!,
string.IsNullOrWhiteSpace(ban.reason) ? "You were banned from this room." : $"You were banned from this room: {ban.reason}");
return new Success(true, null, ban);
}
[HttpDelete("{id}/bans/{bannedPlayerId}"), HttpDelete("{id}/bans"),
HttpPost("{id}/bans/{bannedPlayerId}"), HttpPut("{id}/bans/{bannedPlayerId}"), Authorize]
public async Task<Success> RemoveRoomBan(long? id, long? bannedPlayerId)
{
var userId = User.GetUserId();
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
if (!canModify) return Success.Error("Insufficient permissions");
long targetPlayer = bannedPlayerId ?? 0;
string[] idKeys =
{
"bannedPlayerId", "bannedPlayerID", "bannedId", "bannedid", "banned_id",
"playerId", "playerid", "player_id", "player",
"bannerId", "bannerid", "banner_id",
"userId", "userid", "id"
};
if (targetPlayer <= 0 && Request.HasFormContentType)
foreach (var key in idKeys)
if (long.TryParse(Request.Form[key], out var formPlayer)) { targetPlayer = formPlayer; break; }
if (targetPlayer <= 0)
foreach (var key in idKeys)
if (long.TryParse(Request.Query[key], out var queryPlayer)) { targetPlayer = queryPlayer; break; }
if (targetPlayer <= 0)
{
try
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
var raw = await reader.ReadToEndAsync();
if (!string.IsNullOrWhiteSpace(raw) && JsonNode.Parse(raw) is JsonObject body)
targetPlayer = ParsePlayerId(body, idKeys);
}
catch { }
}
if (targetPlayer <= 0)
{
return Success.Error("bannedPlayerId is required");
}
var ban = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer);
if (ban == null) return Success.Error("Player is not banned from this room");
db.RoomBans.Remove(ban);
await db.SaveChangesAsync();
return new Success(true, null, ban);
}
[HttpPut("{id}/loadscreen")]
public async Task<Success> SetLoadingScreen(long id, string imageName, string title, string subtitle)
{
var userId = User.GetUserId();
if (userId == null) return new Success(false, "Invalid auth", null);
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
if (!canModify) return Success.Error("Insufficient permissions");
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
if (room == null) return Success.Error("Room not found");
var entry = new RoomLoadScreen { ImageName = imageName, RoomId = room.RoomId, Subtitle = subtitle, Title = title };
List<RoomLoadScreen> loadScreen = room.loadScreen.FromJson<List<RoomLoadScreen>>() ?? new();
if (loadScreen.Count >= 1)
loadScreen[0] = entry;
else
loadScreen.Add(entry);
room.loadScreen = loadScreen.ToJson();
await db.SaveChangesAsync();
var fullRoom = await RoomUtils.GetRoom((long)id!);
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
return new Success(true, null, fullRoom);
}
private static long ParsePlayerId(JsonObject body, string[] idKeys)
{
foreach (var prop in body)
{
if (prop.Value is not JsonValue value) continue;
bool keyMatches = false;
foreach (var key in idKeys)
if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; }
if (!keyMatches) continue;
if (value.TryGetValue<long>(out var longId)) return longId;
if (value.TryGetValue<string>(out var strId) && long.TryParse(strId, out var parsed)) return parsed;
if (value.TryGetValue<double>(out var doubleId) && doubleId > 0 && doubleId == Math.Floor(doubleId)) return (long)doubleId;
}
return 0;
}
private static string ParseJsonString(JsonObject body, params string[] keys)
{
foreach (var prop in body)
{
if (prop.Value is not JsonValue value) continue;
bool keyMatches = false;
foreach (var key in keys)
if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; }
if (keyMatches && value.TryGetValue<string>(out var str)) return str ?? "";
}
return "";
}
}
}