Relays: PrivaPub reads from the relays its configuration names

Federation:Relays names relays by their actor (or inbox) address; the instance actor follows Public at each, as Mastodon
subscribes, a minute after start and every six hours (asked again a day after no answer or a refusal, undone when a
relay is no longer named). What an accepted relay passes on comes to the federated timeline and nobody's home: a public
post it forwards (Activity-Relay), read again from its origin like any forwarded post, and a post it announces
(aode-relay), kept as its author's and never as the relay's boost. Nothing of a persona's is sent to a relay; sending
public posts there waits for the owner.

The pasture gains both relays (peers/relay.sh, peers/aoderelay.sh) and scenarios/relay.sh, 13 checks. The village
backlog is clean: 2574 checks pass, one known gap (Misskey's).

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-05 23:11:59 +02:00
1 parent 1c7d1ece9c
commit f6fc0c535c
22 files changed
+542 -24

No files matched your search

@@ -38,9 +38,12 @@ namespace PrivaPub.Federation.Inbox.Handlers
// handler is one of them; tests set it
public IEnumerable<IActivityHandler> Relays { get; set; }
readonly PrivaPub.Federation.Relays.IRelays _relayService;
public AnnounceHandler(DbEntities dbEntities, ILocalActorService localActors, IRemotePosts remotePosts, IFanout fanout, IRemoteActorService remoteActors,
IObjectRecords records, IQuoteService quotes, IServiceProvider services = default)
IObjectRecords records, IQuoteService quotes, IServiceProvider services = default, PrivaPub.Federation.Relays.IRelays relays = default)
{
_relayService = relays;
_services = services;
_records = records;
_quotes = quotes;
@@ -74,6 +77,11 @@ namespace PrivaPub.Federation.Inbox.Handlers
return;
}
if (_relayService != default && await _relayService.Passes(actor.ActorURI, token))
{
await FromRelay(objectUri, token);
return;
}
var isLocal = objectUri.StartsWith(_localActors.BaseAddress + "/", StringComparison.OrdinalIgnoreCase);
var original = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && !p.DeletedAt.HasValue && p.ReblogOfPostId == null).ExecuteFirstAsync(token);
var followed = await _dbEntities.Followings.Match(f => f.TargetActorURI == actor.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token);
@@ -139,6 +147,27 @@ namespace PrivaPub.Federation.Inbox.Handlers
await _fanout.Distribute(reblog, token);
}
// a post a relay we subscribe to announces: kept as its author's, for the federated timeline, never as the relay's
// boost (Mastodon unwraps them the same way)
async Task FromRelay(string objectUri, CancellationToken token)
{
var held = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri).ExecuteFirstAsync(token);
var post = held ?? await _remotePosts.StoreContext(objectUri, 0, token);
if (post == default)
{
Arrival.Drop("fetch-failed");
return;
}
Arrival.About(post.ObjectType ?? "Note", post.Visibility, post.CreationDate);
if (held != default)
{
Arrival.Drop("duplicate");
return;
}
Arrival.Accept("relayed");
await _fanout.Distribute(post, token);
}
async Task GroupActivity(JsonNode inner, ForeignAvatar group, CancellationToken token)
{
Arrival.About(Value(inner, "type"));