R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
Build / Build (push) Successful in 1m3s

A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media
overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the
web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits.
/clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the
staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its
personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published,
Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids.
Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones.

Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 04:02:47 +02:00
1 parent f67e2ae35e
commit 70d1ffe9a2
46 files changed
+2223 -107

No files matched your search

+41 -9
View File
@@ -18,6 +18,10 @@ namespace PrivaPub.Federation.Relays
{
public interface IRelays
{
/// <summary>The relays Federation:Relays names, as it names them.</summary>
IReadOnlyList<string> Configured { get; }
/// <summary>The relays named by the configuration or by the server's settings, each once.</summary>
IReadOnlyList<string> Named { get; }
Task Reconcile(CancellationToken token);
Task<bool> Answered(JsonNode activity, ForeignAvatar actor, bool accepted, CancellationToken token);
Task<bool> Passes(string actorUri, CancellationToken token);
@@ -25,9 +29,10 @@ namespace PrivaPub.Federation.Relays
}
// Relays (Activity-Relay, aode-relay, pub-relay): the instance actor follows Public at each relay the configuration
// names, as Mastodon subscribes, and takes what they pass on: public posts, forwarded as their authors sent them (read
// again from their origin, as any forwarded post) or announced by the relay. A persona's public posts go to the relays
// that accepted us (owner decision 2026-10-06), as Mastodon sends them; nothing else of a persona's does.
// (Federation:Relays) or the server's settings (ServerSettings.Relays, from the administrator's page) name, as Mastodon
// subscribes, and takes what they pass on: public posts, forwarded as their authors sent them (read again from their
// origin, as any forwarded post) or announced by the relay. A persona's public posts go to the relays that accepted us
// (owner decision 2026-10-06), as Mastodon sends them; nothing else of a persona's does.
public class Relays : IRelays
{
static readonly TimeSpan AskAgain = TimeSpan.FromDays(1);
@@ -36,31 +41,58 @@ namespace PrivaPub.Federation.Relays
readonly IRemoteActorService _remoteActors;
readonly ILocalActorService _localActors;
readonly IDeliveryService _delivery;
readonly Domain.Instance.IInstanceSettings _settings;
readonly SemaphoreSlim _reconciling = new(1, 1);//the hourly round and the administrator's page, one at a time
volatile HashSet<string> _accepted;
public Relays(IOptions<FederationOptions> options, IRemoteActorService remoteActors, ILocalActorService localActors, IDeliveryService delivery)
public Relays(IOptions<FederationOptions> options, IRemoteActorService remoteActors, ILocalActorService localActors, IDeliveryService delivery,
Domain.Instance.IInstanceSettings settings)
{
_options = options;
_remoteActors = remoteActors;
_localActors = localActors;
_delivery = delivery;
_settings = settings;
}
// subscribes to the relays the configuration names (again a day after an unanswered or refused request), and
// unsubscribes from those it no longer names
public IReadOnlyList<string> Configured => Clean(_options.Value.Relays);
public IReadOnlyList<string> Named => Clean(Configured.Concat(_settings.Current.Relays ?? new()));
static List<string> Clean(IEnumerable<string> addresses) =>
(addresses ?? Array.Empty<string>()).Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()).Distinct(StringComparer.Ordinal).ToList();
// subscribes to the relays the configuration and the settings name (again a day after an unanswered or refused
// request), and unsubscribes from those they no longer name: with an Undo, unless the same relay is still named
// under another address (an Undo would end that subscription too)
public async Task Reconcile(CancellationToken token)
{
var configured = (_options.Value.Relays ?? new()).Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()).Distinct().ToList();
await _reconciling.WaitAsync(token);
try
{
await ReconcileNow(token);
}
finally
{
_reconciling.Release();
}
}
async Task ReconcileNow(CancellationToken token)
{
var configured = Named;
var known = await DB.Default.Find<RelaySubscription>().ExecuteAsync(token);
if (configured.Count == 0 && known.Count == 0)
return;
var instance = await _localActors.GetInstanceActor(token);
var kept = known.Where(k => configured.Contains(k.Configured)).Select(k => k.ActorURI).Where(a => a != default).ToHashSet(StringComparer.Ordinal);
foreach (var gone in known.Where(k => !configured.Contains(k.Configured)))
{
if (gone.State != RelayState.Rejected && !string.IsNullOrEmpty(gone.InboxURL))
if (gone.State != RelayState.Rejected && !string.IsNullOrEmpty(gone.InboxURL) && !kept.Contains(gone.ActorURI))
await _delivery.Enqueue(instance, new[] { gone.InboxURL }, Undo(instance, gone.FollowActivityURI), token);
await DB.Default.DeleteAsync<RelaySubscription>(gone.ID);
}
_accepted = default;
foreach (var address in configured)
{
var subscription = known.FirstOrDefault(k => k.Configured == address);
@@ -143,7 +175,7 @@ namespace PrivaPub.Federation.Relays
};
}
// subscribes to the configured relays a minute after start, then every six hours
// subscribes to the relays named (configuration and settings) a minute after start, then every six hours
public sealed class RelaySubscriber(IServiceScopeFactory scopes, ILogger<RelaySubscriber> logger) : BackgroundService
{
static readonly TimeSpan Every = TimeSpan.FromHours(6);