using PrivaPub.Models.Post; using System.Globalization; using System.Text.Json.Nodes; using static PrivaPub.Federation.Objects.ActivityJson; namespace PrivaPub.Federation.Objects { public sealed class NoteDocument { public string Id { get; init; } public string Type { get; init; } public string AttributedTo { get; init; } public string ContentHtml { get; init; } public string Title { get; init; } public string SpoilerText { get; init; } public bool Sensitive { get; init; } public string Language { get; init; } public string Url { get; init; } public string InReplyTo { get; init; } public string Context { get; init; } public string Audience { get; init; } public string QuoteUri { get; init; } public DateTime Published { get; init; } public DateTime? Updated { get; init; } public IReadOnlyList To { get; init; } = Array.Empty(); public IReadOnlyList Cc { get; init; } = Array.Empty(); public IReadOnlyList Mentions { get; init; } = Array.Empty(); public IReadOnlyList Tags { get; init; } = Array.Empty(); public IReadOnlyList Attachments { get; init; } = Array.Empty(); } public static class NoteParser { static readonly string[] NoteTypes = { "Note", "Article", "Page", "Question", "Event", "Video", "Audio", "Image" }; const int MaxAttachments = 16; const int MaxTags = 64; public static NoteDocument Parse(JsonNode node) { if (node is not JsonObject note) return default; var type = Value(note, "type"); var id = Id(note); if (!NoteTypes.Contains(type) || Origin.Of(id) == default) return default; var (contentHtml, language) = Content(note); var summary = Value(note, "summary"); return new NoteDocument { Id = id, Type = type, AttributedTo = Attribution(note["attributedTo"]), ContentHtml = ContentSanitizer.Html(contentHtml), Title = Plain(Value(note, "name"), 500), SpoilerText = Plain(summary, 500), Sensitive = note["sensitive"] is JsonValue sensitive && sensitive.TryGetValue(out var s) && s || !string.IsNullOrWhiteSpace(summary), Language = language, Url = UrlOf(note["url"]) ?? id, InReplyTo = Id(note["inReplyTo"]), Context = Id(note["context"]) ?? Value(note, "conversation"), Audience = Id(note["audience"]), QuoteUri = Value(note, "quote") ?? Value(note, "quoteUrl") ?? Value(note, "quoteUri") ?? Value(note, "_misskey_quote"), Published = Time(Value(note, "published")) ?? DateTime.UtcNow, Updated = Time(Value(note, "updated")), To = List(note["to"]), Cc = List(note["cc"]), Mentions = Tags(note, "Mention").Select(t => new PostMention { ActorURI = Value(t, "href"), Handle = Value(t, "name") }) .Where(m => Origin.Of(m.ActorURI) != default) .DistinctBy(m => m.ActorURI) .Take(MaxTags) .ToList(), Tags = Tags(note, "Hashtag").Select(t => Value(t, "name")?.TrimStart('#').ToLowerInvariant()) .Where(t => !string.IsNullOrEmpty(t)) .Distinct() .Take(MaxTags) .ToList(), Attachments = Attachments(note) }; } static (string Html, string Language) Content(JsonObject note) { var content = Value(note, "content"); string language = default; if (note["contentMap"] is JsonObject map && map.Count > 0) { var first = map.First(); language = first.Key; if (string.IsNullOrEmpty(content) && first.Value is JsonValue value && value.TryGetValue(out var mapped)) content = mapped; } if (string.IsNullOrEmpty(content) && Value(note, "_misskey_content") is { } misskey) content = System.Net.WebUtility.HtmlEncode(misskey).Replace("\n", "
"); return (content, language); } static string Attribution(JsonNode node) => node switch { JsonArray array => array.OfType().Where(o => Value(o, "type") is "Person" or "Service" or "Application" or "Organization").Select(Id) .Concat(array.OfType().Select(Id)) .Concat(array.OfType().Where(o => Value(o, "type") == "Group").Select(Id)) .FirstOrDefault(id => id != default), _ => Id(node) }; static IEnumerable Tags(JsonObject note, string type) => (note["tag"] switch { JsonArray array => array.OfType(), JsonObject single => new[] { single }, _ => Enumerable.Empty() }).Where(t => Value(t, "type") == type); static List Attachments(JsonObject note) { var items = note["attachment"] switch { JsonArray array => array.OfType(), JsonObject single => new[] { single }, _ => Enumerable.Empty() }; return items .Where(a => Value(a, "type") is "Document" or "Image" or "Video" or "Audio") .Select(a => new PostMedia { RemoteURL = UrlOf(a["url"]), ContentType = Value(a, "mediaType"), Description = Plain(Value(a, "name") ?? Value(a, "summary"), 1500), Blurhash = Value(a, "blurhash"), Width = Int(a["width"]), Height = Int(a["height"]), Focus = a["focalPoint"] is JsonArray focus && focus.Count == 2 ? focus.Select(f => f is JsonValue v && v.TryGetValue(out var d) ? (float)Math.Clamp(d, -1, 1) : 0f).ToArray() : default }) .Where(m => Origin.Of(m.RemoteURL) != default) .Take(MaxAttachments) .ToList(); } static string UrlOf(JsonNode node) => node switch { JsonValue value when value.TryGetValue(out var text) => text, JsonObject obj => Value(obj, "href") ?? Value(obj, "id"), JsonArray array => array.Select(item => item is JsonObject link && Value(link, "mediaType") is { } mediaType && !mediaType.StartsWith("text/html") ? default : UrlOf(item)) .FirstOrDefault(url => url != default) ?? array.Select(UrlOf).FirstOrDefault(url => url != default), _ => default }; static IReadOnlyList List(JsonNode node) => node switch { JsonArray array => array.Select(Id).Where(id => id != default).Distinct(StringComparer.Ordinal).ToList(), JsonNode single when Id(single) is { } id => new[] { id }, _ => Array.Empty() }; static DateTime? Time(string value) => DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed) ? parsed.UtcDateTime : default; static int? Int(JsonNode node) => node is JsonValue value && value.TryGetValue(out var number) && number is > 0 and < 100_000 ? number : (int?)null; static string Plain(string value, int max) { if (string.IsNullOrWhiteSpace(value)) return default; value = value.Trim(); return value.Length <= max ? value : value[..max]; } } }