diff --git a/PrivaPub.Tests/Http/MastodonStatusesTests.cs b/PrivaPub.Tests/Http/MastodonStatusesTests.cs index 66bdba4..a8222f3 100644 --- a/PrivaPub.Tests/Http/MastodonStatusesTests.cs +++ b/PrivaPub.Tests/Http/MastodonStatusesTests.cs @@ -1,3 +1,6 @@ +using MongoDB.Entities; + +using PrivaPub.Models.User; using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; @@ -385,6 +388,28 @@ namespace PrivaPub.Tests.Http Assert.Equal(1, status.Number("reblogs_count")); } + [Fact] + public async Task A_boost_reaches_the_authors_server_once_through_its_shared_inbox_and_a_like_its_own_inbox() + { + var since = DateTime.UtcNow.AddSeconds(-1); + var alice = await _host.Mastodon("alice"); + var bob = new RemoteActor(_peer, "bob"); + var remote = await _host.PublicPostFrom(bob, alice); + await DB.Default.Update().Match(f => f.ActorURI == bob.Id).Modify(f => f.SharedInboxURL, bob.SharedInbox) + .ExecuteAsync(TestContext.Current.CancellationToken); + + (await alice.Client.Post($"/api/v1/statuses/{remote.ID}/reblog")).Ok(); + (await alice.Client.Post($"/api/v1/statuses/{remote.ID}/unreblog")).Ok(); + (await alice.Client.Post($"/api/v1/statuses/{remote.ID}/favourite")).Ok(); + + var shared = await Jobs.Deliveries(bob.SharedInbox, since, TestContext.Current.CancellationToken); + Assert.Single(shared, d => d.Type() == "Announce"); + Assert.Single(shared, d => d.Type() == "Undo"); + var own = await bob.Delivered(since); + Assert.DoesNotContain(own, d => d.Type() is "Announce" or "Undo"); + Assert.Single(own, d => d.Type() == "Like"); + } + [Fact] public async Task Bookmarks_are_kept_listed_and_removed() { diff --git a/PrivaPub/Domain/Statuses/StatusService.cs b/PrivaPub/Domain/Statuses/StatusService.cs index fb2f36e..47c8c61 100644 --- a/PrivaPub/Domain/Statuses/StatusService.cs +++ b/PrivaPub/Domain/Statuses/StatusService.cs @@ -487,7 +487,7 @@ namespace PrivaPub.Domain.Statuses if (!original.IsFederatedCopy) await Notifications.Add(original.GroupUserId, NotificationType.Reblog, me.Id, me.Uri, original.ID, token); await _fanout.Distribute(reblog, token); - await _delivery.EnqueueToFollowers(me, announce, token, await AuthorInbox(original, token) is { } inbox ? new[] { inbox } : default); + await _delivery.EnqueueToFollowers(me, announce, token, await AuthorInbox(original, token, shared: true) is { } inbox ? new[] { inbox } : default); return new StatusOutcome(reblog); } @@ -497,7 +497,7 @@ namespace PrivaPub.Domain.Statuses await DB.Default.DeleteAsync(e => e.PostId == existing.ID); await DB.Default.Update().MatchID(original.ID).Modify(b => b.Inc(p => p.ReblogsCount, -1)).ExecuteAsync(token); await _delivery.EnqueueToFollowers(me, Undo(me, announce, $"undo-announce-{existing.ID}"), token, - await AuthorInbox(original, token) is { } authorInbox ? new[] { authorInbox } : default); + await AuthorInbox(original, token, shared: true) is { } authorInbox ? new[] { authorInbox } : default); return new StatusOutcome(original); } @@ -595,11 +595,14 @@ namespace PrivaPub.Domain.Statuses return post != default && await VisibilityPolicy.CanSee(post, me.Id, token) ? post : default; } - async Task AuthorInbox(PostEntity post, CancellationToken token) + // shared: what goes to the author's followers too (a boost) goes to its server's shared inbox, so the server gets + // one copy, as Mastodon sends it; two copies at once race in servers that count as they process (Misskey) + async Task AuthorInbox(PostEntity post, CancellationToken token, bool shared = false) { if (!post.IsFederatedCopy || string.IsNullOrEmpty(post.AuthorAccountId)) return default; - return (await _dbEntities.ForeignAvatars.MatchID(post.AuthorAccountId).ExecuteFirstAsync(token))?.InboxURL; + var author = await _dbEntities.ForeignAvatars.MatchID(post.AuthorAccountId).ExecuteFirstAsync(token); + return shared && !string.IsNullOrEmpty(author?.SharedInboxURL) ? author.SharedInboxURL : author?.InboxURL; } async Task MayPost(Models.Group.Group group, LocalActor author, CancellationToken token)