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.OrdinalIgnoreCase));
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,
};
}
| | | | | | |