using Markdig; using PrivaPub.Models.Federation; using System.Globalization; using System.Text.Json.Nodes; using PostEntity = PrivaPub.Models.Post.Post; using DmPostEntity = PrivaPub.Models.Post.DmPost; namespace PrivaPub.Services.Federation { public static class ActivityPubRenderer { public const string ActivityStreams = "https://www.w3.org/ns/activitystreams"; public const string Public = "https://www.w3.org/ns/activitystreams#Public"; static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().DisableHtml().Build(); public static string Html(string markdown) => string.IsNullOrEmpty(markdown) ? string.Empty : Markdown.ToHtml(markdown, Pipeline).Trim(); public static string Timestamp(DateTime value) => DateTime.SpecifyKind(value, DateTimeKind.Utc).ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); public static JsonObject Actor(LocalActor actor) { var document = new JsonObject { ["@context"] = new JsonArray(ActivityStreams, "https://w3id.org/security/v1"), ["id"] = actor.Uri, ["type"] = actor.Kind switch { LocalActorKind.Group => "Group", LocalActorKind.Application => "Application", _ => "Person" }, ["preferredUsername"] = actor.UserName, ["name"] = actor.Name, ["summary"] = Html(actor.Summary), ["url"] = actor.Uri, ["inbox"] = actor.Inbox, ["outbox"] = actor.Outbox, ["followers"] = actor.Followers, ["following"] = actor.Following, ["published"] = Timestamp(actor.Published), ["manuallyApprovesFollowers"] = actor.ManuallyApprovesFollowers, ["discoverable"] = actor.Discoverable, ["endpoints"] = new JsonObject { ["sharedInbox"] = actor.SharedInbox }, ["publicKey"] = new JsonObject { ["id"] = actor.KeyId, ["owner"] = actor.Uri, ["publicKeyPem"] = Keys.ToSubjectPublicKeyInfoPem(actor.PublicKeyPem) } }; if (!string.IsNullOrEmpty(actor.PictureURL)) document["icon"] = new JsonObject { ["type"] = "Image", ["url"] = actor.PictureURL }; if (!string.IsNullOrEmpty(actor.ThumbnailURL)) document["image"] = new JsonObject { ["type"] = "Image", ["url"] = actor.ThumbnailURL }; return document; } public static JsonObject Note(PostEntity post, LocalActor author, LocalActor group, string inReplyTo) { var to = new JsonArray(Public); var cc = new JsonArray(author.Followers); if (group != default) cc.Add(group.Uri); var note = NoteBody(author.PostUri(post.ID), author, post.Title, post.Text, post.HasContentWarning, post.CreationDate, to, cc, inReplyTo); if (group != default) note["audience"] = group.Uri; return note; } public static JsonObject DirectNote(DmPostEntity post, LocalActor author, IReadOnlyList<(string Uri, string Handle)> recipients, string context) { var note = NoteBody(author.PostUri(post.ID), author, post.Title, post.Text, post.HasContentWarning, post.CreationDate, new JsonArray(recipients.Select(r => (JsonNode)r.Uri).ToArray()), new JsonArray(), default); note["tag"] = new JsonArray(recipients.Select(r => (JsonNode)new JsonObject { ["type"] = "Mention", ["href"] = r.Uri, ["name"] = "@" + r.Handle }).ToArray()); note["content"] = string.Join(" ", recipients.Select(r => $"@{r.Handle}")) + " " + note["content"]!.GetValue(); if (!string.IsNullOrEmpty(context)) note["context"] = context; return note; } static JsonObject NoteBody(string id, LocalActor author, string title, string text, bool sensitive, DateTime published, JsonArray to, JsonArray cc, string inReplyTo) { var note = new JsonObject { ["id"] = id, ["type"] = "Note", ["attributedTo"] = author.Uri, ["content"] = Html(text), ["published"] = Timestamp(published), ["url"] = id, ["to"] = to, ["cc"] = cc, ["sensitive"] = sensitive }; if (sensitive && !string.IsNullOrEmpty(title)) note["summary"] = title; else if (!string.IsNullOrEmpty(title)) note["name"] = title; if (!string.IsNullOrEmpty(inReplyTo)) note["inReplyTo"] = inReplyTo; return note; } public static JsonObject Create(LocalActor actor, JsonObject note, string activityId) => new() { ["@context"] = ActivityStreams, ["id"] = actor.ActivityUri(activityId), ["type"] = "Create", ["actor"] = actor.Uri, ["published"] = note["published"]?.DeepClone(), ["to"] = note["to"]?.DeepClone(), ["cc"] = note["cc"]?.DeepClone(), ["object"] = note }; public static JsonObject Announce(LocalActor group, string objectUri, string activityId) => new() { ["@context"] = ActivityStreams, ["id"] = group.ActivityUri(activityId), ["type"] = "Announce", ["actor"] = group.Uri, ["published"] = Timestamp(DateTime.UtcNow), ["to"] = new JsonArray(Public), ["cc"] = new JsonArray(group.Followers), ["object"] = objectUri }; public static JsonObject Delete(LocalActor actor, string objectUri, string activityId, JsonArray to, JsonArray cc) => new() { ["@context"] = ActivityStreams, ["id"] = actor.ActivityUri(activityId), ["type"] = "Delete", ["actor"] = actor.Uri, ["to"] = to, ["cc"] = cc, ["object"] = new JsonObject { ["id"] = objectUri, ["type"] = "Tombstone" } }; public static JsonObject Accept(LocalActor actor, JsonNode follow, string activityId) => new() { ["@context"] = ActivityStreams, ["id"] = actor.ActivityUri(activityId), ["type"] = "Accept", ["actor"] = actor.Uri, ["object"] = follow.DeepClone() }; public static JsonObject OrderedCollection(string id, int totalItems, IEnumerable items) { var collection = new JsonObject { ["@context"] = ActivityStreams, ["id"] = id, ["type"] = "OrderedCollection", ["totalItems"] = totalItems }; if (items != default) collection["orderedItems"] = new JsonArray(items.ToArray()); return collection; } } }