using System.Text.Json; using DorkNet.Server.Data; using DorkNet.Server.Data.Entities; using Microsoft.EntityFrameworkCore; namespace DorkNet.Server.Services; /// /// A sub-room's role-permission matrix: the cells the 2023 client reads from /// GET photon_access_token (ALIIIAGAPND.Permissions, a list of /// BNLPBMJJOMM) and writes with /// PUT rooms/{id}/subrooms/{sub}/permissions. /// /// The served list is the built-in defaults with every stored cell for /// the room's sub-room applied on top: a stored cell replaces the default /// with the same (Role, Permission), and cells with no default are appended. /// The list must never be empty — with no Permissions the room-permissions /// runtime (OMJKHJLFOCO) falls into its persisted-role migration path, which /// NREs during first spawn. /// /// Cell keys (formatter RecNet.Runtime/HDOJMEBCAGP.txt): /// Permission (string), Role (OMMBGJMJJPN), Override /// (bool), Type (FEOJFGECKGH: Bool 0 / Int 1 / StringList 2), /// Value (string). Role and Type go out as numbers; requests are /// accepted with numbers, numeric strings or enum names. /// public static class RoomPermissionMatrix { /// One matrix cell. public sealed record Cell(string Permission, int Role, bool Override, int Type, string Value); /// The defaults every sub-room starts from: Co-Owner and Creator /// may use the maker pen, spawn inventions and consumables, reset the /// room and toggle play gizmos. public static IReadOnlyList Defaults { get; } = BuildDefaults(); private static IReadOnlyList BuildDefaults() { var permissions = new[] { "13", // Can Use Maker Pen "20", // Can Spawn Inventions "21", // Can Spawn Consumables "22", // Can Use Room Reset Button "23", // Can Use Play Gizmos Toggle }; var list = new List(); foreach (var role in new[] { RoomRoleRules.WireCoOwner, RoomRoleRules.WireCreator }) foreach (var permission in permissions) list.Add(new Cell(permission, role, true, 0, "true")); return list; } /// Defaults merged with the stored cells. public static List Merge(IEnumerable stored) { var cells = Defaults.ToList(); foreach (var row in stored.OrderBy(r => r.Id)) { var cell = new Cell(row.Permission, row.Role, row.Override, row.Type, row.Value); var at = cells.FindIndex(c => c.Role == row.Role && string.Equals(c.Permission, row.Permission, StringComparison.Ordinal)); if (at >= 0) cells[at] = cell; else cells.Add(cell); } return cells; } /// The served matrix for one sub-room (by OrderIndex). public static async Task> LoadAsync(DorkNetDbContext db, long roomId, long subRoomOrderIndex) { var stored = await db.RoomRolePermissions.AsNoTracking() .Where(p => p.RoomId == roomId && p.SubRoomId == subRoomOrderIndex) .ToListAsync(); return Merge(stored); } /// The ALIIIAGAPND body: {RoomInstanceId, /// PhotonAccessToken, Permissions}. Used both by /// GET photon_access_token and the PhotonAccessToken hub /// push (IBEOONPEELF.txt:519 registers it as Action<ALIIIAGAPND>). public static Dictionary AccessTokenBody(long roomInstanceId, IEnumerable cells) => new() { ["RoomInstanceId"] = roomInstanceId, ["PhotonAccessToken"] = string.Empty, ["Permissions"] = cells.Select(ToWire).ToList(), }; public static object ToWire(Cell c) => new { c.Permission, c.Role, c.Override, c.Type, c.Value, }; /// Parse the PUT body: a JSON array of cells (an object wrapping /// a Permissions array is accepted too). Returns null when the body /// is not JSON or holds no array. public static List? Parse(string body) { if (string.IsNullOrWhiteSpace(body)) return null; JsonDocument doc; try { doc = JsonDocument.Parse(body); } catch (JsonException) { return null; } using (doc) { var root = doc.RootElement; if (root.ValueKind == JsonValueKind.Object) { root = root.EnumerateObject() .Where(p => p.Name.Equals("Permissions", StringComparison.OrdinalIgnoreCase)) .Select(p => p.Value) .FirstOrDefault(); } if (root.ValueKind != JsonValueKind.Array) return null; var cells = new List(); foreach (var item in root.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object) continue; string? permission = null; int? role = null; bool @override = false; int type = 0; string value = string.Empty; foreach (var prop in item.EnumerateObject()) { switch (prop.Name.ToLowerInvariant()) { case "permission": permission = Scalar(prop.Value)?.Trim(); break; case "role": role = EnumValue(prop.Value, RoleName); break; case "type": type = EnumValue(prop.Value, TypeName) ?? 0; break; case "override": @override = prop.Value.ValueKind == JsonValueKind.True || (prop.Value.ValueKind == JsonValueKind.String && bool.TryParse(prop.Value.GetString(), out var b) && b); break; case "value": value = Scalar(prop.Value) ?? string.Empty; break; } } if (string.IsNullOrEmpty(permission) || role is null) continue; if (permission.Length > 128 || value.Length > 4096) continue; cells.Add(new Cell(permission, role.Value, @override, type, value)); } return cells; } } private static string? Scalar(JsonElement e) => e.ValueKind switch { JsonValueKind.String => e.GetString(), JsonValueKind.Number => e.GetRawText(), JsonValueKind.True => "true", JsonValueKind.False => "false", JsonValueKind.Null or JsonValueKind.Undefined => null, _ => e.GetRawText(), }; private static int? EnumValue(JsonElement e, Func byName) { if (e.ValueKind == JsonValueKind.Number && e.TryGetInt32(out var n)) return n; if (e.ValueKind != JsonValueKind.String) return null; var s = e.GetString() ?? string.Empty; return int.TryParse(s, out n) ? n : byName(s); } private static int? RoleName(string s) => s.Trim().ToLowerInvariant() switch { "none" => RoomRoleRules.WireNone, "banned" => RoomRoleRules.WireBanned, "host" => RoomRoleRules.WireHost, "moderator" => RoomRoleRules.WireModerator, "coowner" => RoomRoleRules.WireCoOwner, "temporarycoowner" => RoomRoleRules.WireTemporaryCoOwner, "creator" => RoomRoleRules.WireCreator, _ => null, }; private static int? TypeName(string s) => s.Trim().ToLowerInvariant() switch { "bool" => 0, "int" => 1, "stringlist" => 2, _ => null, }; }