Post gains what federation and the Mastodon API need: Visibility (Public, Unlisted, FollowersOnly, Direct, Circle, LocalGeo), the author's account id, to/cc, the Create's id, url, context, quote, InReplyToURI and the parent's author, a separate SpoilerText next to the title, language, mentions, hashtags, remote attachments (alt text, blurhash, focus, size), reply/favourite/reblog counters, revisions, EditedAt and DeletedAt. Direct messages are Posts with Visibility Direct and a ConversationId; migration _005 copies DmPost rows across with their ids and fills the new fields of existing posts. DmPost is left in place so a rollback still sees the old messages. Inbound: - NoteParser reads Note, Article, Page, Question and media types: content, then contentMap, then _misskey_content; summary as the spoiler and name as the title; Mention and Hashtag tags; attachments; a PeerTube-style list attribution prefers the person over the channel; quote URIs. - Addressing classifies like Mastodon, finding followers-only by the author's own followers URL (now stored on ForeignAvatar), not by a "/followers" suffix. - Create keeps a post when a local persona is addressed or mentioned, when it replies to a local post (the parent's reply count goes up) or when a community it follows is addressed; an unsolicited public post is not stored. Update keeps the previous version as a revision. The outbox and object endpoints serve only Public and Unlisted posts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
83 lines
3.0 KiB
C#
83 lines
3.0 KiB
C#
using System.Text.Json;
|
|
|
|
using PrivaPub.Federation.Objects;
|
|
|
|
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 IReadOnlyList<ActorKey> Keys { get; init; } = Array.Empty<ActorKey>();
|
|
|
|
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)
|
|
};
|
|
}
|
|
|
|
static List<ActorKey> ParseKeys(JsonElement root, string actorId)
|
|
{
|
|
var keys = new List<ActorKey>();
|
|
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<JsonElement>()
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
}
|