A community announces back to its author's own server

Lemmy keeps its user's post in a community pending until the community announces it back. A community hosted here
announced only to its followers, so a post from a server where nobody follows the community stayed pending there; the
author's own server (its shared inbox) now gets the announces too. INTEROP records, confirmed live, why Lemmy refuses
our Flags (an Application reporter, no `to`, an array object). FEDERATION.md's custom emoji line is put back before the
replies paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 16:29:04 +02:00
1 parent a3a66b6db2
commit af2c61292b
5 files changed
+40 -8

No files matched your search

+3 -2
View File
@@ -140,7 +140,8 @@ A group is either a **community** or a **circle**.
- A **community** follows [FEP-1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md). Posts - A **community** follows [FEP-1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md). Posts
addressed to it (in `to`, `cc` or `audience`) are accepted according to its posting policy (followers, anyone, or addressed to it (in `to`, `cc` or `audience`) are accepted according to its posting policy (followers, anyone, or
moderators only) and the group `Announce`s the whole activity, with `audience` set, to its followers. A new post is moderators only) and the group `Announce`s the whole activity, with `audience` set, to its followers and to the
author's own server, whether anyone there follows it or not (Lemmy keeps its user's post pending until then). A new post is
also announced as an object so Mastodon shows it. Updates and deletes of community content are announced too. A also announced as an object so Mastodon shows it. Updates and deletes of community content are announced too. A
top-level post is a `Page` with a `name`. Members are counted at `/flock`; moderators are listed at `/wardens`, which top-level post is a `Page` with a `name`. Members are counted at `/flock`; moderators are listed at `/wardens`, which
the actor's `attributedTo` points to, with `postingRestrictedToMods` as Lemmy expects. A mention of a community posts into it. the actor's `attributedTo` points to, with `postingRestrictedToMods` as Lemmy expects. A mention of a community posts into it.
@@ -291,13 +292,13 @@ a post first met after its edits has them, and an edit that carries them brings
- **Shown** to clients as GoToSocial's `interaction_policy` (`can_favourite`, `can_reply`, `can_reblog`). - **Shown** to clients as GoToSocial's `interaction_policy` (`can_favourite`, `can_reply`, `can_reblog`).
- Our own posts state only `canQuote`: anyone may reply to, like and boost them. - Our own posts state only `canQuote`: anyone may reply to, like and boost them.
**Custom emoji** (`Emoji` tags) are read on posts, display names, bios and profile fields, at most 64 per object.
**Replies its author approves** (FEP-5624's `canReply`, as PeerTube sets it on a video whose comments are moderated; a **Replies its author approves** (FEP-5624's `canReply`, as PeerTube sets it on a video whose comments are moderated; a
`null` one says nothing): a persona it names, or that the post mentions, may reply, and the reply waits `null` one says nothing): a persona it names, or that the post mentions, may reply, and the reply waits
(`privapub.approval: pending`), its `Create` going to the author alone. The author's `ApproveReply`, signed by the author (`privapub.approval: pending`), its `Create` going to the author alone. The author's `ApproveReply`, signed by the author
and naming the post answered, lets it out to its audience with `replyApproval`; a `RejectReply` leaves it ours alone and naming the post answered, lets it out to its audience with `replyApproval`; a `RejectReply` leaves it ours alone
(`rejected`). An empty `canReply` refuses every reply (422). (`rejected`). An empty `canReply` refuses every reply (422).
**Custom emoji** (`Emoji` tags) are read on posts, display names, bios and profile fields, at most 64 per object.
**Profiles** keep their header, profile fields, `manuallyApprovesFollowers`, `published`, `movedTo`, `indexable`, **Profiles** keep their header, profile fields, `manuallyApprovesFollowers`, `published`, `movedTo`, `indexable`,
`memorial` and avatar and header descriptions. A post's title becomes `name` and is also the first, bold line of `content`, because Mastodon does not show `memorial` and avatar and header descriptions. A post's title becomes `name` and is also the first, bold line of `content`, because Mastodon does not show
`name`. A content warning without its own text uses the title, or "Content warning". `name`. A content warning without its own text uses the title, or "Content warning".
+15
View File
@@ -125,6 +125,21 @@ namespace PrivaPub.Tests.Federation
Assert.True(await DB.Default.Find<Post>().Match(p => p.GroupId == community.Id && p.ActorURI == stranger.Id).ExecuteAnyAsync(token)); Assert.True(await DB.Default.Find<Post>().Match(p => p.GroupId == community.Id && p.ActorURI == stranger.Id).ExecuteAnyAsync(token));
} }
// Lemmy keeps its user's post pending until the community announces it back, whether anyone there follows the
// community or not
[Fact]
public async Task A_post_into_a_community_is_announced_back_to_its_authors_server_though_nobody_there_follows_it()
{
var (_, owner) = await _harness.Persona("owner");
var (_, community) = await Group(GroupKind.Community, PostingPolicy.Anyone, owner.Id);
var stranger = new RemoteActor(_harness.Peer, "stranger", sharedInbox: true);
await _harness.Deliver(stranger, "/human-centipede", Create(stranger, new[] { community.Uri, Addressing.Public }, community.Uri));
var announces = (await _harness.Outgoing(stranger.SharedInbox)).Where(a => a["actor"]!.GetValue<string>() == community.Uri).ToList();
Assert.Contains(announces, a => a["object"] is JsonObject inner && inner["type"]!.GetValue<string>() == "Create");
}
[Fact] [Fact]
public async Task Mentioning_a_community_posts_into_it_as_a_titled_page() public async Task Mentioning_a_community_posts_into_it_as_a_titled_page()
{ {
+17 -4
View File
@@ -1,6 +1,7 @@
using MongoDB.Entities; using MongoDB.Entities;
using PrivaPub.Federation.Actors; using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Objects;
using PrivaPub.Federation.Rendering; using PrivaPub.Federation.Rendering;
using PrivaPub.Models.Federation; using PrivaPub.Models.Federation;
@@ -18,6 +19,16 @@ namespace PrivaPub.Federation.Outbox
public class GroupDistributor : IGroupDistributor public class GroupDistributor : IGroupDistributor
{ {
// so the announce's id resolves (PeasantsController.Grunt); a repeat of the same announce keeps the first // so the announce's id resolves (PeasantsController.Grunt); a repeat of the same announce keeps the first
// a remote author's server's inbox (its shared one when it has one); none for our own personas
static async Task<string[]> AuthorsInbox(JsonObject activity, CancellationToken token)
{
if (ActivityJson.Id(activity["actor"]) is not { } actorUri)
return default;
var author = await DB.Default.Find<Models.User.ForeignAvatar>().Match(a => a.ActorURI == actorUri).ExecuteFirstAsync(token);
var inbox = string.IsNullOrEmpty(author?.SharedInboxURL) ? author?.InboxURL : author.SharedInboxURL;
return string.IsNullOrEmpty(inbox) ? default : new[] { inbox };
}
static async Task Keep(LocalActor group, JsonObject announce, string objectUri, CancellationToken token) static async Task Keep(LocalActor group, JsonObject announce, string objectUri, CancellationToken token)
{ {
var body = (JsonObject)announce.DeepClone(); var body = (JsonObject)announce.DeepClone();
@@ -63,16 +74,18 @@ namespace PrivaPub.Federation.Outbox
["audience"] = group.Uri, ["audience"] = group.Uri,
["object"] = embedded ["object"] = embedded
}; };
// Every follower, the author's own server included: Lemmy 1.0 keeps its user's post pending until the community // Every follower, and the author's own server whether anyone there follows the community or not: Lemmy 1.0 keeps
// announces it back, and clears that before it answers the echo 400 ("Object is not remote"). // its user's post pending until the community announces it back, and clears that before it answers the echo 400
await _delivery.EnqueueToFollowers(group, announce, token); // ("Object is not remote").
var authorsServer = await AuthorsInbox(activity, token);
await _delivery.EnqueueToFollowers(group, announce, token, authorsServer);
await Keep(group, announce, objectUri, token); await Keep(group, announce, objectUri, token);
// a new post or comment is also boosted as itself: microblogs (Mastodon, Akkoma, Misskey) drop an Announce of an // a new post or comment is also boosted as itself: microblogs (Mastodon, Akkoma, Misskey) drop an Announce of an
// activity, so without it their members never see what is said in the community; Lemmy answers it 400, harmlessly // activity, so without it their members never see what is said in the community; Lemmy answers it 400, harmlessly
if (isNewPost && !string.IsNullOrEmpty(objectUri)) if (isNewPost && !string.IsNullOrEmpty(objectUri))
{ {
var boost = ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}"); var boost = ActivityPubRenderer.Announce(group, objectUri, $"announce-object-{key}");
await _delivery.EnqueueToFollowers(group, boost, token); await _delivery.EnqueueToFollowers(group, boost, token, authorsServer);
await Keep(group, boost, objectUri, token); await Keep(group, boost, objectUri, token);
} }
} }
+3 -1
View File
@@ -503,7 +503,9 @@ on a Page: pins live in `featured`, locks in `Lock`.
- **Anti-spam:** activities for a community are accepted only if a local user follows it. - **Anti-spam:** activities for a community are accepted only if a local user follows it.
- **Actors:** a Create, vote, moderation action or Flag must come from a Person, Service or Organization. **A Group or - **Actors:** a Create, vote, moderation action or Flag must come from a Person, Service or Organization. **A Group or
Application actor fails to parse**, so moderation comes from the moderator Person. Our Flags, sent by an Application actor fails to parse**, so moderation comes from the moderator Person. Our Flags, sent by an
Application instance actor, probably fail *(unconfirmed)*. Application instance actor with no `to` and an array `object` (the account and the post), are refused: confirmed live
2026-10-06, Lemmy 1.0.0-beta.2 answers 400 and keeps no report. Reaching its moderators needs an anonymous
`Service`-typed reporter, `to: [the post's community]` and the post alone as `object`.
- **Flags:** exactly one `to` (community or site); `object` a URL or an array; the reason in `summary` or `content`. - **Flags:** exactly one `to` (community or site); `object` a URL or an array; the reason in `summary` or `content`.
- **Moderation trust:** an action is trusted when it is on the community's or the object's host (FEP-fe34), or when its - **Moderation trust:** an action is trusted when it is on the community's or the object's host (FEP-fe34), or when its
actor is in the moderators list Lemmy fetched. actor is in the moderators list Lemmy fetched.
+2 -1
View File
@@ -661,7 +661,8 @@ it, raw where it doesn't.
ban of a persona refuses its posts there and shows as `blocked_by`), featured, moderators, `Warn`, `Resolve`; ban of a persona refuses its posts there and shows as `blocked_by`), featured, moderators, `Warn`, `Resolve`;
- votes in and out; link posts; flairs; `Feed` actors; community polls; post `Move`; - votes in and out; link posts; flairs; `Feed` actors; community polls; post `Move`;
- the outbound shape Lemmy requires; - the outbound shape Lemmy requires;
- communities we host announce to the author's own instance too; - communities we host announce to the author's own instance too: **done 2026-10-06** (its server's shared inbox,
whether anyone there follows the community or not: Lemmy keeps its user's post pending until it is announced back);
- Flags from a `Service`-typed reporter actor. - Flags from a `Service`-typed reporter actor.
- **GoToSocial interaction policies** (**done 2026-10-05**, `InteractionApprovals`): stored and shown as - **GoToSocial interaction policies** (**done 2026-10-05**, `InteractionApprovals`): stored and shown as
`interaction_policy`; `ReplyRequest`/`LikeRequest`/`AnnounceRequest` where approval is needed; `Accept{result}` `interaction_policy`; `ReplyRequest`/`LikeRequest`/`AnnounceRequest` where approval is needed; `Accept{result}`