diff --git a/CLAUDE.md b/CLAUDE.md index a7f4c33..fa08536 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -525,7 +525,9 @@ group www-data and reaches the private mongod; `sudo -u www-data` works too. Each attempt redoes everything. Refused before anything changed, it is abandoned and recorded, and the server boots as it was; failed midway, the process exits 1 and the next start tries again; after 3 failures it exits 75, which the unit's `RestartPreventExitStatus` leaves down for someone to look. While `restore.json` exists, commands exit 75 (but - `admin restore --status`) and the deploy refuses to run. + `admin restore --status`) and the deploy refuses to run. For `RestoreRecord.FollowersGrace` (14 days) after a restore, + FEP-8fcf rests: no `Collection-Synchronization` header goes out, and a follow only the remote remembers is adopted, + not undone. - **The administrator's page** (`BackupController`, `/clientapi/admin/backups`; owner decision 2026-10-07, which approved these endpoints in production): the list (with what runs, the restore waiting, the last restore's report, logins deleted since each backup), back up now (202; the page polls), delete, and: diff --git a/PrivaPub/Federation/Actors/FollowingSynchronization.cs b/PrivaPub/Federation/Actors/FollowingSynchronization.cs index 7f88f4a..f17d528 100644 --- a/PrivaPub/Federation/Actors/FollowingSynchronization.cs +++ b/PrivaPub/Federation/Actors/FollowingSynchronization.cs @@ -21,7 +21,8 @@ namespace PrivaPub.Federation.Actors // FEP-8fcf, received: an account elsewhere tells, with its delivery, a digest of its followers here. When the personas // following it here digest otherwise, its partial list (read signed by the instance actor) says who they are: a follow // only PrivaPub remembers ends, a request it answered without our knowing is accepted, and a follow only it remembers - // is undone by the persona, as Mastodon does. A follow ends only when the list is the one the digest describes. + // is undone by the persona, as Mastodon does, or kept within RestoreRecord.FollowersGrace of a restore, which may have + // lost it. A follow ends only when the list is the one the digest describes. public class FollowingSynchronization : IJobHandler { const int MaxPages = 10; @@ -87,6 +88,19 @@ namespace PrivaPub.Federation.Actors } if (await _localActors.FindByUri(uri, token) is not { Kind: LocalActorKind.Person } stranger) continue; + // a follow the restore lost: kept, not undone + if (await Infrastructure.Backup.RestoreRecord.InFollowersGrace(token)) + { + await DB.Default.SaveAsync(new Following + { + AvatarId = stranger.Id, + TargetActorURI = actor.ActorURI, + TargetAccountId = actor.ID, + TargetInboxURL = string.IsNullOrEmpty(actor.InboxURL) ? actor.SharedInboxURL : actor.InboxURL, + State = FollowState.Accepted + }, token); + continue; + } // (it has no id for a follow PrivaPub never made; it is undone by actor and object) var undo = new JsonObject { diff --git a/PrivaPub/Federation/Outbox/DeliveryService.cs b/PrivaPub/Federation/Outbox/DeliveryService.cs index 84ff300..9ff3a95 100644 --- a/PrivaPub/Federation/Outbox/DeliveryService.cs +++ b/PrivaPub/Federation/Outbox/DeliveryService.cs @@ -188,10 +188,11 @@ namespace PrivaPub.Federation.Outbox }); } - // FEP-8fcf: what a delivery to the persona's followers tells the receiving server of its followers there + // FEP-8fcf: what a delivery to the persona's followers tells the receiving server of its followers there; nothing for + // a while after a restore, whose losses are not the other server's to mend static async Task<(string, string)[]> Synchronization(LocalActor signer, string body, Uri inbox, CancellationToken token) { - if (!FollowersSynchronization.Synchronizes(signer) || JsonNode.Parse(body) is not JsonObject activity || !FollowersSynchronization.ForFollowers(activity, signer)) + if (!FollowersSynchronization.Synchronizes(signer) || await Infrastructure.Backup.RestoreRecord.InFollowersGrace(token) || JsonNode.Parse(body) is not JsonObject activity || !FollowersSynchronization.ForFollowers(activity, signer)) return []; var there = await FollowersSynchronization.On(signer, FollowersSynchronization.Origin(inbox.AbsoluteUri), token); return [(FollowersSynchronization.Header, FollowersSynchronization.HeaderValue(signer, FollowersSynchronization.Digest(there)))];