A sender's digest of its followers here is compared and mended

FEP-8fcf, received. When a delivery's Collection-Synchronization header digests the sender's followers on PrivaPub
otherwise than the personas following it, a job reads the list the header names (on the sender's origin, signed by the
instance actor): a follow the list leaves out ends, only when the list is the one the digest describes; a request it
lists is taken as accepted; a persona it lists that follows nothing there sends Undo{Follow}, as Mastodon does. Each
claiming delivery is compared once.

Checked live (scenarios/followsync.sh, now 15 checks): PrivaPub ends a follow Mastodon lost and undoes one only
Mastodon remembered.

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-06 07:46:46 +02:00
1 parent bd18de35a6
commit e0682fa868
11 files changed
+291 -12

No files matched your search

@@ -55,6 +55,23 @@ namespace PrivaPub.Federation.Actors
public static string HeaderValue(LocalActor actor, string digest) =>
$"collectionId=\"{actor.Followers}\", url=\"{RollCall(actor)}\", digest=\"{digest}\"";
// what a Collection-Synchronization header claims, written as a Signature header's parameters; default when it
// names no collection, list or digest
public static SynchronizationClaim Claim(string actorUri, string header)
{
var values = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var part in (header ?? "").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
var equals = part.IndexOf('=');
if (equals > 0)
values[part[..equals].Trim()] = part[(equals + 1)..].Trim().Trim('"');
}
return values.TryGetValue("collectionId", out var collection) && values.TryGetValue("url", out var url) && values.TryGetValue("digest", out var digest)
&& digest.Length == 64 && digest.All(Uri.IsHexDigit)
? new SynchronizationClaim(actorUri, collection, url, digest.ToLowerInvariant())
: default;
}
// whether the activity, or the object it carries, is addressed to the actor's followers
public static bool ForFollowers(JsonObject activity, LocalActor actor) =>
Addressed(activity, actor.Followers) || activity["object"] is JsonObject inner && Addressed(inner, actor.Followers);