R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
Build / Build (push) Successful in 1m3s
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:
1 parent
f67e2ae35e
commit
70d1ffe9a2
46 files changed
+2223
-107
No files matched your search
@@ -104,16 +104,21 @@ namespace PrivaPub.Federation.Actors
|
||||
"abuse", "postmaster", "webmaster", "hostmaster", "security", "noreply", "no_reply", "null", "undefined"
|
||||
};
|
||||
|
||||
// what the instance actor says of itself until the administrator describes the server (ServerSettings)
|
||||
const string InstanceSummary = "The instance actor of this PrivaPub server; it signs the requests no other actor speaks for.";
|
||||
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly IOptionsMonitor<AppConfiguration> _appConfiguration;
|
||||
readonly Domain.Instance.IInstanceSettings _settings;
|
||||
InstanceActor _instanceActor;
|
||||
ReporterActor _reporterActor;
|
||||
ReaderActor _readerActor;
|
||||
|
||||
public LocalActorService(DbEntities dbEntities, IOptionsMonitor<AppConfiguration> appConfiguration)
|
||||
public LocalActorService(DbEntities dbEntities, IOptionsMonitor<AppConfiguration> appConfiguration, Domain.Instance.IInstanceSettings settings)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_appConfiguration = appConfiguration;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public string BaseAddress => _appConfiguration.CurrentValue.BackendBaseAddress?.TrimEnd('/');
|
||||
@@ -195,14 +200,18 @@ namespace PrivaPub.Federation.Actors
|
||||
public async Task<LocalActor> GetInstanceActor(CancellationToken token)
|
||||
{
|
||||
var instance = _instanceActor ??= await LoadInstanceActor(token);
|
||||
// the server's name, and its short description once the administrator wrote one of their own
|
||||
var settings = _settings.Current;
|
||||
|
||||
return new LocalActor
|
||||
{
|
||||
Id = instance.ID,
|
||||
Kind = LocalActorKind.Application,
|
||||
UserName = InstanceUserName,
|
||||
Name = "PrivaPub",
|
||||
Summary = "The instance actor of this PrivaPub server; it signs the requests no other actor speaks for.",
|
||||
Name = settings.Title,
|
||||
Summary = string.IsNullOrWhiteSpace(settings.ShortDescription) || settings.ShortDescription == ServerSettings.DefaultDescription
|
||||
? InstanceSummary
|
||||
: settings.ShortDescription,
|
||||
PrivateKeyPem = instance.PrivateKey,
|
||||
PublicKeyPem = instance.PublicKey,
|
||||
Discoverable = false,
|
||||
|
||||
@@ -23,10 +23,13 @@ namespace PrivaPub.Federation.Controllers
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly IOptionsMonitor<RegistrationOptions> _registrations;
|
||||
readonly Domain.Instance.IInstanceSettings _settings;
|
||||
|
||||
public WellKnownController(ILocalActorService localActors, DbEntities dbEntities, IOptionsMonitor<RegistrationOptions> registrations)
|
||||
public WellKnownController(ILocalActorService localActors, DbEntities dbEntities, IOptionsMonitor<RegistrationOptions> registrations,
|
||||
Domain.Instance.IInstanceSettings settings)
|
||||
{
|
||||
_registrations = registrations;
|
||||
_settings = settings;
|
||||
_localActors = localActors;
|
||||
_dbEntities = dbEntities;
|
||||
}
|
||||
@@ -140,8 +143,9 @@ namespace PrivaPub.Federation.Controllers
|
||||
},
|
||||
["metadata"] = new JsonObject
|
||||
{
|
||||
["nodeName"] = "PrivaPub",
|
||||
["nodeDescription"] = "A small ActivityPub server where one private login keeps several unlinkable public personas.",
|
||||
// the server's name and short description, as its administrator set them (IInstanceSettings)
|
||||
["nodeName"] = _settings.Current.Title,
|
||||
["nodeDescription"] = _settings.Current.ShortDescription ?? string.Empty,
|
||||
["federation"] = new JsonObject { ["document"] = "https://git.thepra.dev/thepra/SocialPub/src/branch/master/FEDERATION.md" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user