A community's comments reach its microblog members

A comment in a community was announced only as Announce{Create}, which
Akkoma, Mastodon and Misskey drop: their members never saw it (the village
found Akkoma's missing). A comment is now also announced as itself, as a
new post already was; Lemmy answers that form 400, harmlessly. Nothing is
decided by the follower's software.

The town's checker expects a followers-only quote to stay with the
followers, and G-0003 covers only Lemmy-hosted communities (their relayed
moderation), since relayed likes count.

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-05 04:12:53 +02:00
1 parent 9268968fd4
commit aecbf620a8
7 files changed
+34 -6

No files matched your search

+22
View File
@@ -90,6 +90,28 @@ namespace PrivaPub.Tests.Federation
Assert.Contains(announces, a => a["object"] is JsonValue);
}
[Fact]
public async Task A_comment_in_a_community_is_announced_activity_and_object_too()
{
var (_, owner) = await _harness.Persona("owner");
var (_, community) = await Group(GroupKind.Community, PostingPolicy.Followers, owner.Id);
var lemmy = new RemoteActor(_harness.Peer, "lemmy");
var akkoma = new RemoteActor(_harness.Peer, "akkoma");
await _harness.FollowedBy(community, lemmy);
await _harness.FollowedBy(community, akkoma);
var thread = Create(lemmy, new[] { community.Uri, Addressing.Public }, community.Uri);
await _harness.Deliver(lemmy, "/human-centipede", thread);
var comment = Create(lemmy, new[] { community.Uri, Addressing.Public }, community.Uri);
comment["object"]!["inReplyTo"] = thread["object"]!["id"]!.GetValue<string>();
await _harness.Deliver(lemmy, "/human-centipede", comment);
var commentId = comment["object"]!["id"]!.GetValue<string>();
var announces = (await _harness.Outgoing(akkoma.SharedInbox)).Where(a => a["actor"]!.GetValue<string>() == community.Uri).ToList();
Assert.Contains(announces, a => a["object"] is JsonObject inner && inner["object"]?["id"]?.GetValue<string>() == commentId);
Assert.Contains(announces, a => a["object"] is JsonValue bare && bare.GetValue<string>() == commentId);
}
[Fact]
public async Task An_open_community_takes_posts_from_anyone()
{
+1 -1
View File
@@ -262,7 +262,7 @@ namespace PrivaPub.Domain.Statuses
if (create != default)
await _outbox.Publish(author, post, create, token);
if (create != default && group is { IsCircle: false } && visibility is PostVisibility.Public or PostVisibility.Unlisted)
await _groups.Announce(group, create, post.ObjectURI, isNewPost: string.IsNullOrEmpty(post.InReplyToURI), token);
await _groups.Announce(group, create, post.ObjectURI, isNewPost: true, token);
return new StatusOutcome(post);
}
@@ -188,7 +188,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
if (conversation != default)
await DB.Default.Update<DmGroup>().MatchID(conversation.ID).Modify(g => g.UpdatedAt, DateTime.UtcNow).ExecuteAsync(token);
if (group is { IsCircle: false })
await _groups.Announce(group, activity.AsObject(), note.Id, isNewPost: string.IsNullOrEmpty(note.InReplyTo), token);
await _groups.Announce(group, activity.AsObject(), note.Id, isNewPost: true, token);
}
async Task<bool> IsCircleMember(LocalActor circle, string actorUri, CancellationToken token) =>
@@ -67,6 +67,8 @@ namespace PrivaPub.Federation.Outbox
// announces it back, and clears that before it answers the echo 400 ("Object is not remote").
await _delivery.EnqueueToFollowers(group, announce, 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
// 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}");
+2 -1
View File
@@ -297,7 +297,8 @@ Firefish is dead (its site has answered 410 since February 2025).
- **Visibility:** Misskey recognises followers-only by the author's own `followers` URL, matched exactly. Otherwise:
- **A "specified" (direct) note with no resolvable recipients that Misskey fetches by URL is stored as public.**
Circle objects must therefore never be served to an unauthorised fetcher. They aren't: 404.
- **Groups:** vanilla Misskey drops `Announce{Create}`, so groups should `Announce` the Note itself. We send both.
- **Groups:** vanilla Misskey drops `Announce{Create}`, so groups should `Announce` the Note itself. We send both, for
comments as for posts (Akkoma and Mastodon drop the activity form too; the town found Akkoma members missing them).
- **Reactions:** must be `:name:` with no host, plus an Emoji tag, or they fall back to ❤.
- **Article/Page titles** are never shown in Misskey's web UI.
- **Iceshrimp.NET:**
+2 -1
View File
@@ -114,7 +114,8 @@ class World:
who |= self.follows.get(o["author"], set())
if o.get("parent") and o["parent"] in self.objects:
who.add(self.objects[o["parent"]]["author"])
if o.get("quote") and o["quote"] in self.objects:
# a quote reaches its quoted author when it is public or unlisted; a followers-only quote stays with the followers
if o.get("quote") and o["quote"] in self.objects and v in ("public", "unlisted"):
who.add(self.objects[o["quote"]]["author"])
elif v == "circle":
who |= self.members[o["group"]]
+4 -2
View File
@@ -36,13 +36,15 @@
"title": "Votes and moderation that a Lemmy community relays inside Announce (Like, Dislike, Undo, Delete, Block) are dropped",
"match": {
"feature": "count\\.like\\.community",
"observer": "privapub"
"observer": "privapub",
"origin": "lemmy"
},
"kind": "server",
"phase": "P7",
"code": "PrivaPub/Federation/Inbox/Handlers/AnnounceHandler.cs:203",
"opened": "2026-10-01",
"status": "open"
"status": "open",
"note": "Relayed likes count since ed08f80; what stays open is moderation relayed by a Lemmy community (Delete, Block, Lock). Communities hosted here count their likes as any post does."
},
{
"id": "G-0004",