Communities: - a post addressed to a community (to, cc or audience) is accepted according to its posting policy - followers, anyone, or moderators - and GroupDistributor announces the whole activity with `audience` to the community's followers, plus the object for new posts so Mastodon shows them; updates and deletes of community content are announced too; - top-level posts are Pages with a name (the title, or a headline from the text); /flock counts members, /wardens lists moderators; - a Mastodon client posts into a community by mentioning it, or into a remote group, which sets `audience`; - an Announce of an activity from a remote group a persona follows (Lemmy) is followed through: the object is fetched from its own origin, kept with its AudienceURI, and fanned out to the group's local followers; updates are applied in place and deletes checked against the origin. Circles stop being local-only: an undiscoverable Group actor whose follows are all requests the owner approves; posts addressed to the circle and its /flock and delivered to members' own inboxes, never announced, never public; a remote member's post into the circle is accepted from members only. SignedFetchAuthorizer serves circle posts and collections only to a signed request from a member or a member server's instance actor - 404 for anyone else. Circles never surface in search, lookups, mentions, account ids or profile pages. Federation:SecureMode requires a valid signature on every GET under /peasants except the instance actor. Group forms take a posting policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
49 lines
1.6 KiB
C#
49 lines
1.6 KiB
C#
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Federation.Rendering;
|
|
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Federation.Outbox
|
|
{
|
|
public interface IGroupDistributor
|
|
{
|
|
Task Announce(LocalActor group, JsonObject activity, string objectUri, bool isNewPost, CancellationToken token);
|
|
}
|
|
|
|
public class GroupDistributor : IGroupDistributor
|
|
{
|
|
readonly IDeliveryService _delivery;
|
|
|
|
public GroupDistributor(IDeliveryService delivery)
|
|
{
|
|
_delivery = delivery;
|
|
}
|
|
|
|
public async Task Announce(LocalActor group, JsonObject activity, string objectUri, bool isNewPost, CancellationToken token)
|
|
{
|
|
if (group is not { IsCircle: false, Kind: Models.Federation.LocalActorKind.Group })
|
|
return;
|
|
var embedded = (JsonObject)activity.DeepClone();
|
|
embedded.Remove("@context");
|
|
embedded["audience"] ??= group.Uri;
|
|
var key = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(activity["id"]?.GetValue<string>() ?? embedded.ToJsonString())))[..24];
|
|
var announce = new JsonObject
|
|
{
|
|
["@context"] = ActivityPubRenderer.Context(),
|
|
["id"] = group.ActivityUri($"announce-{key}"),
|
|
["type"] = "Announce",
|
|
["actor"] = group.Uri,
|
|
["to"] = new JsonArray(ActivityPubRenderer.Public),
|
|
["cc"] = new JsonArray(group.Followers),
|
|
["audience"] = group.Uri,
|
|
["object"] = embedded
|
|
};
|
|
await _delivery.EnqueueToFollowers(group, announce, token);
|
|
if (isNewPost && !string.IsNullOrEmpty(objectUri))
|
|
await _delivery.EnqueueToFollowers(group, ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}"), token);
|
|
}
|
|
}
|
|
}
|