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:
1 parent
a3a66b6db2
commit
af2c61292b
5 files changed
+40
-8
No files matched your search
+3
-2
@@ -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
|
||||
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
|
||||
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.
|
||||
@@ -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`).
|
||||
- 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
|
||||
`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
|
||||
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).
|
||||
|
||||
**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`,
|
||||
`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".
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// 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]
|
||||
public async Task Mentioning_a_community_posts_into_it_as_a_titled_page()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
using PrivaPub.Models.Federation;
|
||||
|
||||
@@ -18,6 +19,16 @@ namespace PrivaPub.Federation.Outbox
|
||||
public class GroupDistributor : IGroupDistributor
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
var body = (JsonObject)announce.DeepClone();
|
||||
@@ -63,16 +74,18 @@ namespace PrivaPub.Federation.Outbox
|
||||
["audience"] = group.Uri,
|
||||
["object"] = embedded
|
||||
};
|
||||
// Every follower, the author's own server included: Lemmy 1.0 keeps its user's post pending until the community
|
||||
// announces it back, and clears that before it answers the echo 400 ("Object is not remote").
|
||||
await _delivery.EnqueueToFollowers(group, announce, token);
|
||||
// Every follower, and the author's own server whether anyone there follows the community or not: Lemmy 1.0 keeps
|
||||
// its user's post pending until the community announces it back, and clears that before it answers the echo 400
|
||||
// ("Object is not remote").
|
||||
var authorsServer = await AuthorsInbox(activity, token);
|
||||
await _delivery.EnqueueToFollowers(group, announce, token, authorsServer);
|
||||
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
|
||||
// activity, so without it their members never see what is said in the community; Lemmy answers it 400, harmlessly
|
||||
if (isNewPost && !string.IsNullOrEmpty(objectUri))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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.
|
||||
- **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 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`.
|
||||
- **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.
|
||||
|
||||
+2
-1
@@ -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`;
|
||||
- votes in and out; link posts; flairs; `Feed` actors; community polls; post `Move`;
|
||||
- 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.
|
||||
- **GoToSocial interaction policies** (**done 2026-10-05**, `InteractionApprovals`): stored and shown as
|
||||
`interaction_policy`; `ReplyRequest`/`LikeRequest`/`AnnounceRequest` where approval is needed; `Accept{result}`
|
||||
|
||||
Reference in new issue
Block a user