Communities follow FEP-1b12, circles federate to their members only

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
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 12:30:57 +02:00
1 parent 0ccbcd558f
commit a5d9a89445
32 files changed
+652 -58

No files matched your search

+12
View File
@@ -116,6 +116,7 @@ namespace PrivaPub.Services
PrivateKey = privateKey,
PublicKey = publicKey,
Kind = form.IsCommunity ? GroupKind.Community : GroupKind.Circle,
PostingPolicy = Policy(form.PostingPolicy) ?? PostingPolicy.Followers,
IsDiscoverable = form.IsDiscoverable,
ManuallyApprovesMembers = form.ManuallyApprovesMembers,
OwnerAvatarId = form.AvatarId,
@@ -157,6 +158,8 @@ namespace PrivaPub.Services
group.Description = form.Description;
if (form.IsDiscoverable.HasValue)
group.IsDiscoverable = form.IsDiscoverable.Value;
if (Policy(form.PostingPolicy) is { } policy)
group.PostingPolicy = policy;
if (form.ManuallyApprovesMembers.HasValue)
group.ManuallyApprovesMembers = form.ManuallyApprovesMembers.Value;
if (form.RemoveInvitationPassword)
@@ -316,6 +319,14 @@ namespace PrivaPub.Services
!string.IsNullOrEmpty(rootUserId) && !string.IsNullOrEmpty(avatarId)
&& await _dbEntities.RootToAvatars.Match(ra => ra.RootId == rootUserId && ra.AvatarId == avatarId).ExecuteAnyAsync(token);
static PostingPolicy? Policy(string value) => value?.ToLowerInvariant() switch
{
"anyone" => PostingPolicy.Anyone,
"moderators" => PostingPolicy.Moderators,
"followers" => PostingPolicy.Followers,
_ => (PostingPolicy?)null
};
static string NewInvitationCode() => $"{Guid.NewGuid():N}{Guid.NewGuid():N}";
ViewGroup ToView(GroupEntity group, string avatarId)
@@ -331,6 +342,7 @@ namespace PrivaPub.Services
Url = actor.Uri,
Handle = actor.Handle,
IsCommunity = group.Kind == GroupKind.Community,
PostingPolicy = group.PostingPolicy.ToString().ToLowerInvariant(),
IsDiscoverable = group.IsDiscoverable,
ManuallyApprovesMembers = group.ManuallyApprovesMembers,
IsOwner = group.OwnerAvatarId == avatarId,