using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; using PrivaPub.Federation.Objects; using PrivaPub.Models.Post; namespace PrivaPub.Federation.Actors { public sealed record ActorKey(string Id, string Pem); public sealed class ActorDocument { static readonly string[] ActorTypes = { "Person", "Service", "Application", "Group", "Organization" }; public string Id { get; init; } public string Type { get; init; } public string PreferredUsername { get; init; } public string Name { get; init; } public string Summary { get; init; } public string Url { get; init; } public string Inbox { get; init; } public string Outbox { get; init; } public string Followers { get; init; } public string Following { get; init; } public string SharedInbox { get; init; } public string Icon { get; init; } public bool Discoverable { get; init; } = true; public bool Indexable { get; init; } public bool Locked { get; init; } public bool Memorial { get; init; } public string Header { get; init; } public string IconDescription { get; init; } public string HeaderDescription { get; init; } public string MovedTo { get; init; } public DateTime? Published { get; init; } public List Emojis { get; init; } = new(); public Dictionary Fields { get; init; } = new(); public IReadOnlyList Keys { get; init; } = Array.Empty(); public ActorKey Key(string keyId) => Keys.FirstOrDefault(k => k.Id == keyId); public static ActorDocument Parse(JsonElement root) { if (root.ValueKind != JsonValueKind.Object) return default; var id = RemoteActorService.Text(root, "id"); var type = RemoteActorService.Text(root, "type"); if (Origin.Of(id) == default || !ActorTypes.Contains(type)) return default; var inbox = RemoteActorService.Text(root, "inbox"); return new ActorDocument { Id = id, Type = type, PreferredUsername = RemoteActorService.Text(root, "preferredUsername"), Name = RemoteActorService.Text(root, "name"), Summary = RemoteActorService.Text(root, "summary"), Url = RemoteActorService.Text(root, "url") ?? id, Inbox = Origin.Same(inbox, id) ? inbox : default, Outbox = RemoteActorService.Text(root, "outbox"), Followers = RemoteActorService.Text(root, "followers"), Following = RemoteActorService.Text(root, "following"), SharedInbox = root.TryGetProperty("endpoints", out var endpoints) ? RemoteActorService.Text(endpoints, "sharedInbox") : default, Icon = root.TryGetProperty("icon", out var icon) ? RemoteActorService.Text(icon, "url") : default, Discoverable = !root.TryGetProperty("discoverable", out var discoverable) || discoverable.ValueKind != JsonValueKind.False, Keys = ParseKeys(root, id), Indexable = Flag(root, "indexable"), Locked = Flag(root, "manuallyApprovesFollowers"), Memorial = Flag(root, "memorial"), Header = root.TryGetProperty("image", out var image) ? ObjectShapes.Image(JsonNode.Parse(image.GetRawText())) : default, IconDescription = root.TryGetProperty("icon", out var described) ? Alt(described) : default, HeaderDescription = root.TryGetProperty("image", out var headerImage) ? Alt(headerImage) : default, MovedTo = RemoteActorService.Text(root, "movedTo"), Published = DateTimeOffset.TryParse(RemoteActorService.Text(root, "published"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var published) ? published.UtcDateTime : default(DateTime?), Emojis = root.TryGetProperty("tag", out var tags) ? ObjectShapes.Emojis(JsonNode.Parse(tags.GetRawText())) : new(), Fields = root.TryGetProperty("attachment", out var attachment) ? ParseFields(JsonNode.Parse(attachment.GetRawText())) : new() }; } const int MaxFields = 16; static bool Flag(JsonElement root, string property) => root.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.True; static string Alt(JsonElement image) => image.ValueKind == JsonValueKind.Object ? ObjectShapes.Text(RemoteActorService.Text(image, "name") ?? RemoteActorService.Text(image, "summary"), 1500) : default; static Dictionary ParseFields(JsonNode attachment) { var fields = new Dictionary(); foreach (var field in ObjectShapes.Objects(attachment).Where(a => ActivityJson.Value(a, "type") is "PropertyValue" or "Note")) { var name = ObjectShapes.Text(ActivityJson.Value(field, "name"), 255); var value = ActivityJson.Value(field, "value") ?? ActivityJson.Value(field, "content"); if (name == default || value == default || fields.ContainsKey(name)) continue; fields[name] = ContentSanitizer.Html(value.Length <= 2048 ? value : value[..2048]); if (fields.Count == MaxFields) break; } return fields; } static List ParseKeys(JsonElement root, string actorId) { var keys = new List(); if (!root.TryGetProperty("publicKey", out var publicKey)) return keys; var candidates = publicKey.ValueKind switch { JsonValueKind.Object => new[] { publicKey }, JsonValueKind.Array => publicKey.EnumerateArray().Where(k => k.ValueKind == JsonValueKind.Object).ToArray(), _ => Array.Empty() }; foreach (var key in candidates) { var keyId = RemoteActorService.Text(key, "id"); var owner = RemoteActorService.Text(key, "owner"); var pem = RemoteActorService.Text(key, "publicKeyPem"); if (string.IsNullOrEmpty(pem) || owner != actorId || !Origin.Same(keyId, actorId)) continue; keys.Add(new ActorKey(keyId, pem)); } return keys; } } }