Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
namespace RadicadeServer.RecNet.Keepsakes
{
public static class KeepsakeCatalog
{
public sealed record Category(int KeepsakeCategoryId, string VisualId, int LimitPerRoom, int XpValue, string IconOutlineImageName, string IconFilledImageName);
public static readonly Category[] All =
{
new(6, "science_core_red_OLD", 5, 40, "aebsucfhhn98ymh4z25ot8d6x.png", "cxaofjxo76zxa338274fsstof.png"),
new(5, "science_core_blue_OLD", 5, 30, "4w053xrmhi3as5s50fm6kpw6h.png", "bx4ikmxlnmk2tf3z132j94yrd.png"),
new(4, "science_core_yellow_OLD", 5, 20, "08ms9eetb8xhyv9d9nt4ml1lq.png", "0tliyqyr3ckv9h7i5zo6w5sa1.png"),
new(2, "presents", 2, 50, "d5v1c62yb5tzedako4x91u7ll.png", "9abduornso8kkr34frsoyp7wp.png"),
new(0, "explore", 3, 20, "aknjr1eq1hmrx7mr7wyfvll07.png", "701ffxv6rli6btqtvzdh26mse.png"),
};
public static bool Enabled
=> !string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_CATALOG"), "empty", StringComparison.OrdinalIgnoreCase);
public static string? IconOverride
=> NullIfBlank(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ICON"));
public static object[] Results() => Results(Enabled);
public static object[] Results(bool enabled)
{
if (!enabled) return Array.Empty<object>();
var icon = IconOverride;
return All.Select(c => (object)new
{
KeepsakeCategoryId = c.KeepsakeCategoryId,
VisualId = c.VisualId,
LimitPerRoom = c.LimitPerRoom,
XpValue = c.XpValue,
IconOutlineImageName = icon ?? c.IconOutlineImageName,
IconFilledImageName = icon ?? c.IconFilledImageName,
}).ToArray();
}
public static Category? Find(int id) => All.FirstOrDefault(c => c.KeepsakeCategoryId == id);
private static string? NullIfBlank(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value;
}
}
+21
View File
@@ -0,0 +1,21 @@
namespace RadicadeServer.RecNet.Keepsakes;
public static class KeepsakeFeature
{
public static bool? Override { get; set; }
public static bool Enabled
=> Override ?? EnabledFor(KeepsakeRoomConfig.Current, Environment.GetEnvironmentVariable("RADICADE_KEEPSAKES"));
public static bool EnabledFor(KeepsakeRoomConfig keepsakeRooms, string? environment)
{
if (!string.IsNullOrWhiteSpace(environment)) return IsOn(environment);
return keepsakeRooms.Any;
}
private static bool IsOn(string value) => value.Trim().ToLowerInvariant() switch
{
"1" or "true" or "on" or "yes" or "enable" or "enabled" => true,
_ => false,
};
}
+37
View File
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RadicadeServer.RecNet.Keepsakes
{
public class KeepsakeInstance
{
[Key]
public Guid KeepsakeInstanceId { get; set; }
public long RoomId { get; set; }
public long? SubRoomId { get; set; }
public int KeepsakeCategoryId { get; set; }
public long PlacedByAccountId { get; set; }
public DateTime PlacedAt { get; set; }
public bool Removed { get; set; }
}
public class KeepsakeCollectionRecord
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long KeepsakeCollectionRecordId { get; set; }
public long AccountId { get; set; }
public Guid KeepsakeInstanceId { get; set; }
public int KeepsakeCategoryId { get; set; }
public DateTime CollectedAt { get; set; }
}
}
+235
View File
@@ -0,0 +1,235 @@
using System.Text.Json;
using RadicadeServer.DB;
namespace RadicadeServer.RecNet.Keepsakes;
public sealed class KeepsakeRoomConfig
{
public const string FileName = "keepsake-rooms.json";
public const string AllRoomsToken = "*";
public const long DefaultRoomListId = 1;
public const int DefaultRoomLimit = 10;
public const long DefaultEventId = 1;
public const string DefaultEventName = "Science Core";
public IReadOnlySet<long> RoomIds { get; init; } = new HashSet<long>();
public bool AllRooms { get; init; }
public long RoomListId { get; init; } = DefaultRoomListId;
public int RoomLimit { get; init; } = DefaultRoomLimit;
public long EventId { get; init; } = DefaultEventId;
public string EventName { get; init; } = DefaultEventName;
public bool EventEnabled { get; init; } = true;
public bool Any => AllRooms || RoomIds.Count > 0;
public bool IsKeepsakeRoom(long roomId) => AllRooms || RoomIds.Contains(roomId);
public long[] AdvertisedRooms(long roomId)
=> RoomIds.Count == 0 ? new[] { roomId } : RoomIds.OrderBy(id => id).ToArray();
public long[] ConfiguredRooms => RoomIds.OrderBy(id => id).ToArray();
public object[] RoomListEntries(long[] rooms)
=> new object[]
{
new
{
KeepsakeRoomListId = RoomListId,
ProgressionEventId = EventId,
UnlockItemAvatarItemId = (int?)null,
UnlockItemGiftDropId = (int?)null,
UnlockItemLockDurationTicks = (long?)null,
RoomUnlockStartOffsetTicks = 0L,
RoomUnlockIntervalTicks = TimeSpan.TicksPerDay,
RoomUnlockBatchSize = Math.Max(rooms.Length, 1),
RoomType = 0,
KeepsakeRoomLimit = RoomLimit,
KeepsakeRooms = rooms.Select((id, order) => new
{
KeepsakeRoomId = id, RoomId = id, KeepsakeRoomListId = RoomListId,
Type = 0, Order = order
}).ToArray(),
}
};
public static KeepsakeRoomConfig? Override { get; set; }
public static KeepsakeRoomConfig Current => Override ?? Load();
public static KeepsakeRoomConfig Load()
=> Parse(
ReadConfigFile(),
Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOMS"),
Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOM_LIST_ID"),
Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_ROOM_LIMIT"),
Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_ID"),
Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_NAME"));
public static IEnumerable<string> CandidatePaths()
=> DBConfig.AvatarCatalogDirectories()
.Select(directory => Path.Join(directory, FileName))
.Distinct(StringComparer.OrdinalIgnoreCase);
private static string? ReadConfigFile()
{
foreach (var path in CandidatePaths())
{
try
{
if (File.Exists(path)) return File.ReadAllText(path);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
return null;
}
public static KeepsakeRoomConfig Parse(
string? json,
string? rooms = null,
string? roomListId = null,
string? roomLimit = null,
string? eventIdText = null,
string? eventNameText = null)
{
var allRooms = false;
var ids = new HashSet<long>();
var listId = DefaultRoomListId;
var limit = DefaultRoomLimit;
var eventId = DefaultEventId;
var eventName = DefaultEventName;
var eventEnabled = true;
if (!string.IsNullOrWhiteSpace(json))
{
try
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
if (root.ValueKind == JsonValueKind.Array) AddIds(root, ids);
else if (root.ValueKind == JsonValueKind.Object)
{
foreach (var property in root.EnumerateObject())
{
switch (property.Name.ToLowerInvariant())
{
case "allrooms":
allRooms = IsTrue(property.Value);
break;
case "roomids" or "rooms" or "keepsakerooms":
AddIds(property.Value, ids);
break;
case "roomlistid" or "keepsakeroomlistid":
listId = ReadLong(property.Value) ?? listId;
break;
case "roomlimit" or "keepsakeroomlimit":
if (ReadLong(property.Value) is > 0 and <= int.MaxValue and var configuredLimit)
limit = (int)configuredLimit;
break;
case "eventenabled":
eventEnabled = IsTrue(property.Value);
break;
case "eventid" or "progressioneventid":
eventId = ReadLong(property.Value) ?? eventId;
break;
case "eventname" or "progressioneventname":
var name = property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : null;
if (!string.IsNullOrWhiteSpace(name)) eventName = name!.Trim();
break;
}
}
}
}
catch (JsonException)
{
}
}
if (!string.IsNullOrWhiteSpace(rooms))
{
ids.Clear();
var text = rooms.Trim();
allRooms = text == AllRoomsToken;
if (!allRooms) AddIdsFromText(text, ids);
}
if (long.TryParse(roomListId, out var parsedListId)) listId = parsedListId;
if (int.TryParse(roomLimit, out var parsedLimit) && parsedLimit > 0) limit = parsedLimit;
if (long.TryParse(eventIdText, out var parsedEventId)) eventId = parsedEventId;
if (!string.IsNullOrWhiteSpace(eventNameText)) eventName = eventNameText.Trim();
return new KeepsakeRoomConfig
{
RoomIds = ids,
AllRooms = allRooms,
RoomListId = listId,
RoomLimit = limit,
EventId = eventId,
EventName = eventName,
EventEnabled = eventEnabled,
};
}
private static void AddIds(JsonElement element, HashSet<long> ids)
{
switch (element.ValueKind)
{
case JsonValueKind.Array:
foreach (var item in element.EnumerateArray())
{
if (ReadLong(item) is { } id) ids.Add(id);
}
break;
case JsonValueKind.String:
AddIdsFromText(element.GetString(), ids);
break;
default:
if (ReadLong(element) is { } single) ids.Add(single);
break;
}
}
private static void AddIdsFromText(string? text, HashSet<long> ids)
{
if (string.IsNullOrWhiteSpace(text)) return;
foreach (var part in text.Split(new[] { ',', ';', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
if (long.TryParse(part, out var id)) ids.Add(id);
}
}
private static long? ReadLong(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Number when element.TryGetInt64(out var number) => number,
JsonValueKind.String when long.TryParse(element.GetString(), out var parsed) => parsed,
_ => null,
};
private static bool IsTrue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Number when element.TryGetInt64(out var number) => number != 0,
JsonValueKind.String => string.Equals(element.GetString()?.Trim(), "true", StringComparison.OrdinalIgnoreCase)
|| element.GetString()?.Trim() == "1",
_ => false,
};
}
+402
View File
@@ -0,0 +1,402 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Keepsakes;
namespace RadicadeServer.API.Keepsakes;
[ApiController]
[Route("api/keepsakes")]
public sealed class KeepsakesAPI : ControllerBase
{
private readonly RecDBContext? db;
public KeepsakesAPI(RecDBContext db) => this.db = db;
private long? CallerAccountId => ControllerContext?.HttpContext?.User?.GetUserId();
[HttpGet("globalconfig")]
public IActionResult GlobalConfig()
{
var enabled = KeepsakeFeature.Enabled;
return Ok(new
{
KeepsakeFeatureEnabled = enabled,
KeepsakeRoomLimit = enabled ? KeepsakeRoomConfig.Current.RoomLimit : 0,
SocialXpBoostEnabled = false
});
}
[HttpGet("categories")]
public IActionResult Categories()
{
var results = KeepsakeFeature.Enabled ? KeepsakeCatalog.Results() : Array.Empty<object>();
return Ok(new { Results = results, TotalResults = results.Length });
}
[HttpGet("rooms/{roomId:long}")]
public async Task<IActionResult> Room(long roomId)
{
if (db == null) return NoContent();
if (!KeepsakeFeature.Enabled) return NoContent();
var instances = await db.KeepsakeInstances
.Where(i => i.RoomId == roomId && !i.Removed)
.ToListAsync();
var keepsakeRooms = KeepsakeRoomConfig.Current;
var isKeepsakeRoom = keepsakeRooms.IsKeepsakeRoom(roomId);
if (instances.Count == 0 && !isKeepsakeRoom) return NoContent();
var accountId = CallerAccountId;
var records = accountId == null
? new List<KeepsakeCollectionRecord>()
: await db.KeepsakeCollectionRecords
.Where(r => r.AccountId == accountId && instances.Select(i => i.KeepsakeInstanceId).Contains(r.KeepsakeInstanceId))
.ToListAsync();
var collected = records.Select(r => r.KeepsakeInstanceId).ToHashSet();
var uncollected = instances
.Where(i => !collected.Contains(i.KeepsakeInstanceId))
.Select(ToInstancePayload)
.ToArray();
var collectionRecords = records
.Select(r => new
{
AccountId = r.AccountId,
KeepsakeInstanceId = r.KeepsakeInstanceId,
KeepsakeCategoryConfigId = r.KeepsakeCategoryId,
CollectedAt = r.CollectedAt
})
.ToArray();
var payload = new Dictionary<string, object?>
{
["Instances"] = instances.Select(ToInstancePayload).ToArray(),
["KeepsakeInstances"] = uncollected,
["Keepsakes"] = uncollected,
["KeepsakeCollectionRecords"] = collectionRecords,
["CollectionRecords"] = collectionRecords,
["KeepsakeProgressionEventIds"] = isKeepsakeRoom && keepsakeRooms.EventEnabled ? new[] { keepsakeRooms.EventId } : Array.Empty<long>()
};
if (isKeepsakeRoom) AddKeepsakeRoomFields(payload, roomId, keepsakeRooms, instances.Count);
return Ok(payload);
}
private static void AddKeepsakeRoomFields(Dictionary<string, object?> payload, long roomId, KeepsakeRoomConfig keepsakeRooms, int placed)
{
var rooms = keepsakeRooms.AdvertisedRooms(roomId);
payload["KeepsakeRoomId"] = roomId;
payload["KeepsakeRoomLimit"] = keepsakeRooms.RoomLimit;
payload["KeepsakeRoomListId"] = keepsakeRooms.RoomListId;
payload["KeepsakeRooms"] = rooms;
payload["KeepsakeRoomLists"] = keepsakeRooms.RoomListEntries(rooms);
}
[HttpPost("rooms/{roomId:long}")]
public async Task<IActionResult> Place(long roomId)
=> !KeepsakeFeature.Enabled ? NotFound() : await PlaceCore(roomId, await ReadRawBody());
[HttpPost]
public async Task<IActionResult> PlaceFromClient()
{
if (!KeepsakeFeature.Enabled) return NotFound();
var raw = await ReadRawBody();
var roomId = ReadLong(ParseJson(raw), "RoomId");
if (roomId is not > 0) return BadRequest(new { error = "RoomId is required." });
var result = await PlaceCore(roomId.Value, raw);
if (result is not OkObjectResult ok) return result;
return Ok(JsonSerializer.SerializeToElement(ok.Value).GetProperty("KeepsakeInstanceId").GetGuid());
}
private async Task<IActionResult> PlaceCore(long roomId, string raw)
{
if (!KeepsakeFeature.Enabled)
{
return NotFound();
}
if (db == null) return NoContent();
var accountId = CallerAccountId;
if (accountId == null) return Unauthorized();
var config = KeepsakeRoomConfig.Current;
if (!config.IsKeepsakeRoom(roomId)) return BadRequest(new { error = "This room does not have keepsake permissions." });
var body = ParseJson(raw);
var categoryId = ReadInt(body, "KeepsakeCategoryConfigId", "KeepsakeCategoryId", "KeepsakeCategory", "CategoryId");
if (categoryId == null || KeepsakeCatalog.Find(categoryId.Value) == null)
{
return BadRequest(new { error = "Unknown or missing keepsake category." });
}
var instance = new KeepsakeInstance
{
KeepsakeInstanceId = ReadGuid(body, "KeepsakeInstanceId") ?? Guid.NewGuid(),
RoomId = roomId,
SubRoomId = ReadLong(body, "SubRoomId", "KeepsakeSubRoomId"),
KeepsakeCategoryId = categoryId.Value,
PlacedByAccountId = accountId.Value,
PlacedAt = DateTime.UtcNow
};
var existing = await db.KeepsakeInstances.FindAsync(instance.KeepsakeInstanceId);
if (instance.KeepsakeInstanceId == Guid.Empty)
return BadRequest(new { error = "A keepsake instance ID cannot be empty." });
if (existing != null && (existing.RoomId != roomId || existing.PlacedByAccountId != accountId))
return Conflict(new { error = "This keepsake instance belongs to another room or player." });
var otherInstances = db.KeepsakeInstances.Where(i => i.RoomId == roomId && !i.Removed
&& i.KeepsakeInstanceId != instance.KeepsakeInstanceId);
if (await otherInstances.CountAsync() >= config.RoomLimit
|| await otherInstances.CountAsync(i => i.KeepsakeCategoryId == categoryId.Value) >= KeepsakeCatalog.Find(categoryId.Value)!.LimitPerRoom)
return BadRequest(new { error = "Cannot place additional keepsakes in this room." });
if (existing == null) await db.KeepsakeInstances.AddAsync(instance);
else
{
existing.RoomId = instance.RoomId;
existing.SubRoomId = instance.SubRoomId;
existing.KeepsakeCategoryId = instance.KeepsakeCategoryId;
existing.Removed = false;
}
await db.SaveChangesAsync();
return Ok(ToInstancePayload(existing ?? instance));
}
[HttpPost("rooms/{roomId:long}/collect")]
public async Task<IActionResult> Collect(long roomId)
{
if (!KeepsakeFeature.Enabled)
{
return NotFound();
}
if (db == null) return NoContent();
var accountId = CallerAccountId;
if (accountId == null) return Unauthorized();
var raw = await ReadRawBody();
var body = ParseJson(raw);
var wanted = ReadGuidList(body, "KeepsakeInstanceIds", "InstanceIds", "KeepsakeInstanceId");
if (wanted.Count == 0) return BadRequest(new { error = "At least one keepsake instance ID is required." });
var instances = await db.KeepsakeInstances
.Where(i => i.RoomId == roomId && !i.Removed && wanted.Contains(i.KeepsakeInstanceId))
.ToListAsync();
var mine = await db.KeepsakeCollectionRecords
.Where(r => r.AccountId == accountId && wanted.Contains(r.KeepsakeInstanceId))
.Select(r => r.KeepsakeInstanceId)
.ToListAsync();
foreach (var instance in instances.Where(i => !mine.Contains(i.KeepsakeInstanceId)))
{
await db.KeepsakeCollectionRecords.AddAsync(new KeepsakeCollectionRecord
{
AccountId = accountId.Value,
KeepsakeInstanceId = instance.KeepsakeInstanceId,
KeepsakeCategoryId = instance.KeepsakeCategoryId,
CollectedAt = DateTime.UtcNow
});
}
await db.SaveChangesAsync();
return Ok(new
{
KeepsakeRoomId = roomId,
KeepsakeInstanceIds = instances.Select(i => i.KeepsakeInstanceId).ToArray()
});
}
[HttpDelete("rooms/{roomId:long}/{instanceId:guid}")]
public async Task<IActionResult> Remove(long roomId, Guid instanceId)
{
if (!KeepsakeFeature.Enabled) return NotFound();
if (db == null) return NoContent();
var accountId = CallerAccountId;
if (accountId == null) return Unauthorized();
var instance = await db.KeepsakeInstances.FindAsync(instanceId);
if (instance == null || instance.RoomId != roomId) return NotFound();
if (instance.PlacedByAccountId != accountId) return Forbid();
instance.Removed = true;
await db.SaveChangesAsync();
return NoContent();
}
[HttpGet("events")]
public IActionResult Events() => Ok(Array.Empty<object>());
[HttpGet("events/{eventId:long}")]
public async Task<IActionResult> EventInstances(long eventId)
{
var config = KeepsakeRoomConfig.Current;
if (!KeepsakeFeature.Enabled || !config.EventEnabled || eventId != config.EventId) return NotFound();
if (db == null) return Ok(new { Instances = Array.Empty<object>(), CollectionRecords = Array.Empty<object>() });
var instances = await db.KeepsakeInstances.Where(i => !i.Removed).ToListAsync();
instances = instances.Where(i => config.IsKeepsakeRoom(i.RoomId)).ToList();
var accountId = CallerAccountId;
var collected = accountId == null ? new List<Guid>() : await db.KeepsakeCollectionRecords
.Where(r => r.AccountId == accountId).Select(r => r.KeepsakeInstanceId).ToListAsync();
object[] Groups(IEnumerable<KeepsakeInstance> rows) => rows.GroupBy(i => i.RoomId)
.Select(g => (object)new { RoomId = g.Key, KeepsakeInstanceIds = g.Select(i => i.KeepsakeInstanceId).ToArray() }).ToArray();
return Ok(new { Instances = Groups(instances), CollectionRecords = Groups(instances.Where(i => collected.Contains(i.KeepsakeInstanceId))) });
}
[HttpDelete("{instanceId:guid}")]
public async Task<IActionResult> RemoveFromClient(Guid instanceId)
{
if (!KeepsakeFeature.Enabled) return NotFound();
if (CallerAccountId == null) return Unauthorized();
var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId);
return instance == null ? NotFound() : await Remove(instance.RoomId, instanceId);
}
[HttpPost("{instanceId:guid}/collect")]
public async Task<IActionResult> CollectFromClient(Guid instanceId)
{
if (!KeepsakeFeature.Enabled) return CollectionNotFound(instanceId, "Keepsakes are disabled on this server.");
var accountId = CallerAccountId;
if (accountId == null) return Unauthorized();
var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId);
if (instance == null) return CollectionNotFound(instanceId, "The keepsake instance is not registered on this server.");
if (instance.Removed) return CollectionNotFound(instanceId, "The keepsake instance has been removed.");
var newCollection = !await db!.KeepsakeCollectionRecords.AnyAsync(r => r.AccountId == accountId && r.KeepsakeInstanceId == instanceId);
if (newCollection)
{
db.KeepsakeCollectionRecords.Add(new KeepsakeCollectionRecord
{
AccountId = accountId.Value, KeepsakeInstanceId = instanceId,
KeepsakeCategoryId = instance.KeepsakeCategoryId, CollectedAt = DateTime.UtcNow
});
await db.SaveChangesAsync();
}
return Ok(new { TotalXp = newCollection ? KeepsakeCatalog.Find(instance.KeepsakeCategoryId)?.XpValue ?? 0 : 0, SocialBoostXp = 0 });
}
private IActionResult CollectionNotFound(Guid instanceId, string reason)
{
Console.WriteLine($"[Keepsakes] collect instance {instanceId} REFUSED: {reason}");
return Problem(statusCode: StatusCodes.Status404NotFound, title: "Keepsake collection failed", detail: reason);
}
[HttpGet("{**rest}")]
[HttpPost("{**rest}")]
[HttpPut("{**rest}")]
[HttpDelete("{**rest}")]
[HttpPatch("{**rest}")]
public async Task<IActionResult> Unclaimed(string? rest)
{
var body = await ReadRawBody();
return NotFound();
}
private static object ToInstancePayload(KeepsakeInstance instance) => new
{
KeepsakeInstanceId = instance.KeepsakeInstanceId,
KeepsakeCategoryConfigId = instance.KeepsakeCategoryId,
PlacedByAccountId = instance.PlacedByAccountId,
RoomId = instance.RoomId,
SubRoomId = instance.SubRoomId,
KeepsakeRoomId = instance.RoomId,
PlacedAt = instance.PlacedAt
};
private async Task<string> ReadRawBody()
{
using var reader = new StreamReader(Request.Body);
var raw = await reader.ReadToEndAsync();
return raw;
}
private static string LogPreview(string raw) => raw.Length > 2048 ? raw[..2048] + "..." : raw;
private static JsonElement ParseJson(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return default;
try
{
using var document = JsonDocument.Parse(raw);
return document.RootElement.Clone();
}
catch (JsonException)
{
return default;
}
}
private static bool TryProp(JsonElement root, out JsonElement value, params string[] names)
{
if (root.ValueKind == JsonValueKind.Object)
{
foreach (var property in root.EnumerateObject())
{
foreach (var name in names)
{
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
}
}
value = default;
return false;
}
private static int? ReadInt(JsonElement root, params string[] names)
{
if (!TryProp(root, out var value, names)) return null;
return value.ValueKind switch
{
JsonValueKind.Number when value.TryGetInt32(out var number) => number,
JsonValueKind.String when int.TryParse(value.GetString(), out var parsed) => parsed,
_ => null
};
}
private static long? ReadLong(JsonElement root, params string[] names)
{
if (!TryProp(root, out var value, names)) return null;
if (value.ValueKind == JsonValueKind.Null) return null;
return value.ValueKind switch
{
JsonValueKind.Number when value.TryGetInt64(out var number) => number,
JsonValueKind.String when long.TryParse(value.GetString(), out var parsed) => parsed,
_ => null
};
}
private static Guid? ReadGuid(JsonElement root, params string[] names)
{
if (!TryProp(root, out var value, names)) return null;
return value.ValueKind == JsonValueKind.String && Guid.TryParse(value.GetString(), out var parsed)
? parsed
: null;
}
private static List<Guid> ReadGuidList(JsonElement root, params string[] names)
{
var found = new List<Guid>();
if (!TryProp(root, out var value, names)) return found;
switch (value.ValueKind)
{
case JsonValueKind.Array:
found.AddRange(value.EnumerateArray()
.Where(item => item.ValueKind == JsonValueKind.String && Guid.TryParse(item.GetString(), out _))
.Select(item => Guid.Parse(item.GetString()!)));
break;
case JsonValueKind.String when Guid.TryParse(value.GetString(), out var single):
found.Add(single);
break;
}
return found;
}
}