A follow request sent again carries a new id
Lemmy keeps the ids of the activities it received and never answers one again, so resending the same Follow could not heal a follow whose Accept it lost: the village's community follow stayed pending through two resends. Each resend is now the same follow under its own id (<follow id>-again-<n>), and an Accept naming any of them answers the follow; the Undo still embeds the follow, so servers match it by its actor and object. Sent this way, the stuck follow was accepted at once. 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
a5d493a8c9
commit
206506f5e9
5 files changed
+28
-7
No files matched your search
+4
-3
@@ -230,9 +230,10 @@ Posts with a location (shown to nearby users of this server) never leave the ser
|
||||
is read again from its author's server, a deletion of a public or unlisted post is applied once that server answers
|
||||
404 or 410, and anything else is dropped. LD signatures are not verified. A reply forwarded this way is kept when
|
||||
someone here follows the author of the post it answers, as Mastodon does.
|
||||
- **Follow requests** still unanswered are sent again after 15 minutes, an hour, 6 hours, a day, two and four days, the
|
||||
same activity each time: a server can take a Follow and lose its answer (Lemmy 1.0 sends nothing it queued for a
|
||||
server before it started sending there), and one that holds the follow already answers the copy.
|
||||
- **Follow requests** still unanswered are sent again after 15 minutes, an hour, 6 hours, a day, two and four days: a
|
||||
server can take a Follow and lose its answer (Lemmy 1.0 sends nothing it queued for a server before it started
|
||||
sending there), and one that holds the follow answers it again. Each is the same follow under a new id
|
||||
(`…/follow-<id>-again-<n>`), since Lemmy ignores an id it has seen; an answer naming any of them counts.
|
||||
- **Fetching.** All fetches are signed by the instance actor. They go only to public addresses, follow at most three
|
||||
redirects and read at most 1 MB.
|
||||
- **Threads.** A reply's missing parents are fetched, up to 10 levels. When someone here opens a public remote thread,
|
||||
|
||||
@@ -68,12 +68,18 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal(0, await After(TimeSpan.FromMinutes(30)));
|
||||
Assert.Equal(1, await After(TimeSpan.FromMinutes(77)));
|
||||
Assert.Equal(3, await Follows());
|
||||
Assert.All(await _harness.Outgoing(quiet.Id + "/inbox"), a => Assert.Equal(following.FollowActivityURI, a["id"]!.GetValue<string>()));
|
||||
// each one again under its own id (Lemmy never answers an id it has seen), and an answer naming it counts
|
||||
var sentIds = (await _harness.Outgoing(quiet.Id + "/inbox")).Select(a => a["id"]!.GetValue<string>()).ToList();
|
||||
Assert.Equal(new[] { following.FollowActivityURI, following.FollowActivityURI + "-again-1", following.FollowActivityURI + "-again-2" }, sentIds);
|
||||
|
||||
// a request older than the count is sent again too; an answered one never
|
||||
await DB.Default.Update<Following>().MatchID(following.ID).Modify(b => b.Unset(f => f.Resent)).Modify(b => b.Unset(f => f.ResentAt)).ExecuteAsync(token);
|
||||
Assert.Equal(1, await After(TimeSpan.FromDays(1)));
|
||||
await DB.Default.Update<Following>().MatchID(following.ID).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
||||
await _harness.Deliver(quiet, "/human-centipede", new JsonObject
|
||||
{
|
||||
["id"] = NewId(quiet, "accepts"), ["type"] = "Accept", ["actor"] = quiet.Id, ["object"] = following.FollowActivityURI + "-again-2"
|
||||
});
|
||||
Assert.Equal(FollowState.Accepted, (await DB.Default.Find<Following>().OneAsync(following.ID, token)).State);
|
||||
Assert.Equal(0, await After(TimeSpan.FromDays(30)));
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,10 @@ namespace PrivaPub.Domain.Social
|
||||
var inbox = following.TargetInboxURL ?? (await _remoteActors.GetActor(following.TargetActorURI, refresh: false, token))?.InboxURL;
|
||||
if (follower != default && !string.IsNullOrEmpty(inbox))
|
||||
{
|
||||
await _delivery.Enqueue(follower, new[] { inbox }, FollowActivity(follower, following), token,
|
||||
// under a new id each time: Lemmy keeps the ids it received and never answers one again
|
||||
var again = FollowActivity(follower, following);
|
||||
again["id"] = AgainId(following.FollowActivityURI, following.Resent + 1);
|
||||
await _delivery.Enqueue(follower, new[] { inbox }, again, token,
|
||||
again: "resend-" + now.ToString("yyyyMMddHHmm", System.Globalization.CultureInfo.InvariantCulture));
|
||||
sent++;
|
||||
}
|
||||
@@ -308,6 +311,11 @@ namespace PrivaPub.Domain.Social
|
||||
return sent;
|
||||
}
|
||||
|
||||
// a follow sent again is the same follow under a new id, which an answer may name (AcceptHandler.FindFollowing)
|
||||
public const string Again = "-again-";
|
||||
|
||||
public static string AgainId(string followActivityUri, int resend) => $"{followActivityUri}{Again}{resend}";
|
||||
|
||||
static JsonObject FollowActivity(LocalActor follower, Following following) => new()
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
|
||||
@@ -53,7 +53,10 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
if (follow is JsonObject && Value(follow, "type") is { } type && type != "Follow")
|
||||
return default;
|
||||
var followId = Id(follow);
|
||||
// (a follow sent again names the first one under a new id: FollowService.AgainId)
|
||||
var followId = Id(follow) is { } named && named.IndexOf(Domain.Social.FollowService.Again, StringComparison.Ordinal) is var at and > 0
|
||||
? named[..at]
|
||||
: Id(follow);
|
||||
var byId = followId == default
|
||||
? default
|
||||
: await dbEntities.Followings.Match(f => f.FollowActivityURI == followId && f.TargetActorURI == target.ActorURI).ExecuteFirstAsync(token);
|
||||
|
||||
@@ -533,6 +533,9 @@ What it showed:
|
||||
- **Every bare `Announce{object}` is answered 400** (`Failed to parse object`: Lemmy dereferences it expecting an
|
||||
activity), as Lemmy answers the compatibility `Announce(Page)` it sends itself. Both 400s show up as dead deliveries
|
||||
in the statistics.
|
||||
- **It keeps the ids of the activities it received and never answers one again.** A follow it took and whose Accept it
|
||||
lost (sent before its worker for our server started) stayed pending through two resends of the same Follow; sent
|
||||
under a new id (2026-10-05), it was accepted at once (the village's community follow, pending since 05:06).
|
||||
- **Votes travel only to the community**, which relays them as `Announce{Like}` and `Announce{Dislike}`: counted since
|
||||
ed08f80, and a vote turned the other way replaces the first since bbeeda7. A moderator's removal, lock and ban come
|
||||
the same way and are applied since 2026-10-05 (the removal was applied all along; the scenario gave up before Lemmy's
|
||||
|
||||
Reference in new issue
Block a user