178 lines
5.9 KiB
C#
178 lines
5.9 KiB
C#
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 => $"<span class=\"h-card\"><a href=\"{r.Uri}\" class=\"u-url mention\">@{r.Handle}</a></span>"))
|
||
|
|
+ " " + note["content"]!.GetValue<string>();
|
||
|
|
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<JsonNode> items)
|
||
|
|
{
|
||
|
|
var collection = new JsonObject
|
||
|
|
{
|
||
|
|
["@context"] = ActivityStreams,
|
||
|
|
["id"] = id,
|
||
|
|
["type"] = "OrderedCollection",
|
||
|
|
["totalItems"] = totalItems
|
||
|
|
};
|
||
|
|
if (items != default)
|
||
|
|
collection["orderedItems"] = new JsonArray(items.ToArray());
|
||
|
|
return collection;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|