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
|
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
|
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.
|
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
|
- **Follow requests** still unanswered are sent again after 15 minutes, an hour, 6 hours, a day, two and four days: a
|
||||||
same activity each time: a server can take a Follow and lose its answer (Lemmy 1.0 sends nothing it queued for a
|
server can take a Follow and lose its answer (Lemmy 1.0 sends nothing it queued for a server before it started
|
||||||
server before it started sending there), and one that holds the follow already answers the copy.
|
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
|
- **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.
|
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,
|
- **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(0, await After(TimeSpan.FromMinutes(30)));
|
||||||
Assert.Equal(1, await After(TimeSpan.FromMinutes(77)));
|
Assert.Equal(1, await After(TimeSpan.FromMinutes(77)));
|
||||||
Assert.Equal(3, await Follows());
|
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
|
// 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);
|
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)));
|
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)));
|
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;
|
var inbox = following.TargetInboxURL ?? (await _remoteActors.GetActor(following.TargetActorURI, refresh: false, token))?.InboxURL;
|
||||||
if (follower != default && !string.IsNullOrEmpty(inbox))
|
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));
|
again: "resend-" + now.ToString("yyyyMMddHHmm", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
sent++;
|
sent++;
|
||||||
}
|
}
|
||||||
@@ -308,6 +311,11 @@ namespace PrivaPub.Domain.Social
|
|||||||
return sent;
|
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()
|
static JsonObject FollowActivity(LocalActor follower, Following following) => new()
|
||||||
{
|
{
|
||||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
|||||||
{
|
{
|
||||||
if (follow is JsonObject && Value(follow, "type") is { } type && type != "Follow")
|
if (follow is JsonObject && Value(follow, "type") is { } type && type != "Follow")
|
||||||
return default;
|
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
|
var byId = followId == default
|
||||||
? default
|
? default
|
||||||
: await dbEntities.Followings.Match(f => f.FollowActivityURI == followId && f.TargetActorURI == target.ActorURI).ExecuteFirstAsync(token);
|
: 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
|
- **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
|
activity), as Lemmy answers the compatibility `Announce(Page)` it sends itself. Both 400s show up as dead deliveries
|
||||||
in the statistics.
|
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
|
- **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
|
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
|
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