From d6131af289a31dea7ad0b50c41a38101507f1fe2 Mon Sep 17 00:00:00 2001 From: thepra Date: Mon, 5 Oct 2026 00:22:48 +0200 Subject: [PATCH] A public reply names the author it answers in cc A reply whose text had lost the @name of the author it answers (decePub prefills it, a reader may delete it) was addressed to nobody on the author's server. PrivaPub delivered it to the author's inbox anyway, but GoToSocial keeps only what is addressed to someone there, so the reply never appeared under the post. A public or unlisted reply to a remote post now names its parent's author in cc, as Pleroma does; the reply already says whom it answers, so nothing new is revealed. The parent author's actor id is kept on the reply (InReplyToActorURI). Found by decePub's end-to-end tests on the town. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- PrivaPub.Tests/Http/MastodonStatusesTests.cs | 18 ++++++++++++++++++ PrivaPub/Domain/Statuses/StatusService.cs | 1 + .../Rendering/ActivityPubRenderer.cs | 5 +++++ PrivaPub/Models/Post/Post.cs | 1 + 4 files changed, 25 insertions(+) diff --git a/PrivaPub.Tests/Http/MastodonStatusesTests.cs b/PrivaPub.Tests/Http/MastodonStatusesTests.cs index 85b822b..3dc81b3 100644 --- a/PrivaPub.Tests/Http/MastodonStatusesTests.cs +++ b/PrivaPub.Tests/Http/MastodonStatusesTests.cs @@ -163,6 +163,24 @@ namespace PrivaPub.Tests.Http Assert.Equal(post.ID, status.Text("id")); } + // a reader who deletes the @name decePub prefills still answers its author: GoToSocial dropped such a reply, addressed + // to nobody there (found by decePub's e2e tests on the town) + [Fact] + public async Task A_reply_without_the_authors_name_still_names_the_author_in_cc() + { + var since = DateTime.UtcNow.AddSeconds(-1); + var alice = await _host.Mastodon("alice"); + var bob = new RemoteActor(_peer, "bob"); + _peer.WebFinger(bob); + var parent = await _host.PublicPostFrom(bob, alice); + + var reply = await alice.Status("no name in this one", ("in_reply_to_id", parent.ID)); + + Assert.Empty(reply["mentions"]!.AsArray()); + var create = Assert.Single(await bob.Delivered(since), d => d.Type() == "Create"); + Assert.Contains(bob.Id, Strings(create["object"]!["cc"])); + } + [Fact] public async Task A_poll_is_created_validated_and_federated_as_a_question() { diff --git a/PrivaPub/Domain/Statuses/StatusService.cs b/PrivaPub/Domain/Statuses/StatusService.cs index 4269977..5ac9e17 100644 --- a/PrivaPub/Domain/Statuses/StatusService.cs +++ b/PrivaPub/Domain/Statuses/StatusService.cs @@ -198,6 +198,7 @@ namespace PrivaPub.Domain.Statuses AnsweringToPostId = parent?.ID, InReplyToURI = parent?.ObjectURI ?? (IsRemoteUri(draft.InReplyTo) ? draft.InReplyTo : default), InReplyToAccountId = parent?.AuthorAccountId ?? parent?.GroupUserId, + InReplyToActorURI = parent is { IsFederatedCopy: true } ? parent.ActorURI : default, IsLocalOnly = isLocalOnly, ActorURI = author.Uri, Poll = draft.Poll == default ? default : _polls.Create(draft.Poll), diff --git a/PrivaPub/Federation/Rendering/ActivityPubRenderer.cs b/PrivaPub/Federation/Rendering/ActivityPubRenderer.cs index a799efb..098a614 100644 --- a/PrivaPub/Federation/Rendering/ActivityPubRenderer.cs +++ b/PrivaPub/Federation/Rendering/ActivityPubRenderer.cs @@ -146,6 +146,11 @@ namespace PrivaPub.Federation.Rendering }; if (group is { IsCircle: false }) cc.Add(group.Uri); + // a public reply names the author it answers in cc even when the @name was deleted from its text, as Pleroma does: + // GoToSocial keeps only what is addressed to someone there, and the reply already says whom it answers + if (post.Visibility is PostVisibility.Public or PostVisibility.Unlisted && !string.IsNullOrEmpty(post.InReplyToActorURI) + && !mentions.Any(m => m.GetValue() == post.InReplyToActorURI)) + cc.Add(post.InReplyToActorURI); var note = NoteBody(post, author, to, cc, inReplyTo); if (group != default) diff --git a/PrivaPub/Models/Post/Post.cs b/PrivaPub/Models/Post/Post.cs index 4b9a598..05710dc 100644 --- a/PrivaPub/Models/Post/Post.cs +++ b/PrivaPub/Models/Post/Post.cs @@ -10,6 +10,7 @@ namespace PrivaPub.Models.Post public string AnsweringToPostId { get; set; }//the parent's Post.ID when we hold it public string InReplyToURI { get; set; } public string InReplyToAccountId { get; set; } + public string InReplyToActorURI { get; set; }//a local reply's remote parent author, named in its cc public string GroupId { get; set; } public string ConversationId { get; set; }//DmGroup.ID of a direct post public string AudienceURI { get; set; }//a remote community the post was made in