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