Deliveries to followers carry a digest of them, per server
Owner decision of 2026-10-06 (FEP-8fcf). A persona's delivery addressed to its followers carries a signed
Collection-Synchronization header naming its followers, its roll-call (…/groupies/roll-call) and the digest of its
accepted followers on the receiving server only. The roll-call answers a signed request with the persona's followers
on the signer's server and nobody else's.
Mastodon gives every Undo{Follow} it sends after reading a roll-call the same id (…#follows//undo), so a second one
looked like a copy: an Undo of a Follow that comes again while the follow it ends exists again is now kept once per
follow.
Checked live (scenarios/followsync.sh): Mastodon drops a follow PrivaPub lost, and undoes one it lost itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
bf37223849
commit
bd18de35a6
10 files changed
+385
-4
No files matched your search
@@ -199,10 +199,27 @@ namespace PrivaPub.Federation.Inbox
|
||||
// true copy carries the same
|
||||
if (!queued && dedupe != default && CarriesOther(await _queue.Payload(dedupe, token), activity))
|
||||
queued = await _queue.Enqueue(JobKind.ProcessInbox, queuedPayload, host, $"{dedupe}|{Digest(activity)}", token);
|
||||
// an Undo of a Follow that comes again while the follow it ends exists again is meant for that follow: Mastodon
|
||||
// gives every Undo it sends after reading a roll-call (FEP-8fcf) one id, `{actor}#follows//undo`. It is kept
|
||||
// once per follow, so a retry still counts as a copy
|
||||
if (!queued && dedupe != default && type == "Undo" && await FollowAgain(activity, actorUri, token) is { } follow)
|
||||
queued = await _queue.Enqueue(JobKind.ProcessInbox, queuedPayload, host, $"{dedupe}|{Digest(activity)}|{follow}", token);
|
||||
_logger.LogInformation("Inbox {Recipient}: {Type} from {Actor} queued", recipient?.Handle ?? "shared", type, actorUri);
|
||||
return new(StatusCodes.Status202Accepted, Reason: !queued ? "duplicate" : forwardedBy != default ? "forwarded" : "queued");
|
||||
}
|
||||
|
||||
// the follow an Undo of a Follow would end, when there is one: its record's id
|
||||
async Task<string> FollowAgain(JsonNode activity, string actorUri, CancellationToken token)
|
||||
{
|
||||
if (activity["object"] is not JsonObject inner || Value(inner, "type") != "Follow" || Id(inner["object"]) is not { } targetUri
|
||||
|| await _localActors.FindByUri(targetUri, token) is not { } target)
|
||||
return default;
|
||||
var follower = await MongoDB.Entities.DB.Default.Find<Follower>()
|
||||
.Match(f => f.ActorURI == actorUri && f.LocalActorId == target.Id && f.LocalActorKind == target.Kind)
|
||||
.ExecuteFirstAsync(token);
|
||||
return follower?.ID;
|
||||
}
|
||||
|
||||
static string HostOf(string uri) => Uri.TryCreate(uri, UriKind.Absolute, out var parsed) ? parsed.Host.ToLowerInvariant() : default;
|
||||
|
||||
static bool CarriesOther(string earlierPayload, JsonNode activity)
|
||||
|
||||
Reference in new issue
Block a user