using System.Security.Cryptography; using System.Text.Json; namespace PrivaPub.Federation.Actors { public static class ActorFeatures { public static List Detect(JsonElement root) { var found = new List(); if (root.ValueKind != JsonValueKind.Object) return found; void When(bool condition, string feature) { if (condition) found.Add(feature); } bool Has(string name) => root.TryGetProperty(name, out var value) && value.ValueKind is not (JsonValueKind.Null or JsonValueKind.Undefined); bool True(string name) => root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.True; IEnumerable Items(string name) => !root.TryGetProperty(name, out var value) ? Enumerable.Empty() : value.ValueKind == JsonValueKind.Array ? value.EnumerateArray() : new[] { value }; bool Typed(JsonElement item, string type) => item.ValueKind == JsonValueKind.Object && item.TryGetProperty("type", out var t) && t.ValueKind == JsonValueKind.String && t.GetString() == type; When(Has("featured"), "featured"); When(Has("featuredTags"), "featured-tags"); When(Items("alsoKnownAs").Any(), "also-known-as"); When(Has("movedTo"), "moved-to"); When(root.TryGetProperty("endpoints", out var endpoints) && endpoints.ValueKind == JsonValueKind.Object && endpoints.TryGetProperty("sharedInbox", out _), "shared-inbox"); When(Has("assertionMethod"), "fep-521a-assertion-method"); When(Has("proof"), "fep-8b32-proof"); When(Items("attachment").Any(a => Typed(a, "PropertyValue")), "property-value"); When(Items("attachment").Any(a => Typed(a, "VerifiableIdentityStatement") || Typed(a, "IdentityProof")), "fep-c390-identity-proof"); When(Items("tag").Any(t => Typed(t, "Emoji")), "custom-emoji"); When(root.EnumerateObject().Any(p => p.Name.StartsWith("_misskey", StringComparison.Ordinal)), "misskey"); When(True("isCat"), "is-cat"); When(Has("webfinger"), "fep-2c59-webfinger"); When(Has("attributionDomains"), "attribution-domains"); When(Has("searchableBy"), "fep-268d-searchable-by"); When(True("indexable"), "indexable"); When(root.TryGetProperty("discoverable", out var discoverable) && discoverable.ValueKind == JsonValueKind.False, "undiscoverable"); When(True("manuallyApprovesFollowers"), "locked"); When(True("memorial"), "memorial"); When(Has("generator"), "generator"); When(root.TryGetProperty("publicKey", out var keys) && keys.ValueKind == JsonValueKind.Array, "public-key-array"); if (KeyBits(root) is { } bits) found.Add($"key:rsa-{bits}"); return found; } static int? KeyBits(JsonElement root) { if (!root.TryGetProperty("publicKey", out var key)) return default; if (key.ValueKind == JsonValueKind.Array) key = key.EnumerateArray().FirstOrDefault(k => k.ValueKind == JsonValueKind.Object); if (key.ValueKind != JsonValueKind.Object || !key.TryGetProperty("publicKeyPem", out var pem) || pem.ValueKind != JsonValueKind.String) return default; try { using var rsa = RSA.Create(); rsa.ImportFromPem(pem.GetString()); return rsa.KeySize; } catch (Exception ex) when (ex is ArgumentException or CryptographicException) { return default; } } } }