A boost reaches the author's server once

A boost and its undo went to the author's personal inbox and to the shared
inbox of its server's followers: two copies at once, which Misskey counted
twice as it processed them and undid once (seen in decePub's e2e boost on
Misskey). The author's server now gets them through its shared inbox, as
Mastodon sends them; a like still goes to the author's own inbox.

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 02:57:33 +02:00
1 parent cafd2e6db3
commit 864dae4469
2 files changed
+32 -4

No files matched your search

@@ -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<ForeignAvatar>().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()
{
+7 -4
View File
@@ -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<TimelineEntry>(e => e.PostId == existing.ID);
await DB.Default.Update<PostEntity>().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<string> 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<string> 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<bool> MayPost(Models.Group.Group group, LocalActor author, CancellationToken token)