Two owner decisions of 2026-10-05, recorded in the roadmap.
Replies passed on ("the fediverse is broken without"): a public or unlisted
reply from another server to a persona's public, unlisted or followers-only
post goes on to the persona's followers as its author's server sent it, as
Mastodon forwards it, never to the replier's own server, never for a
local-only or group post; its edit and deletion follow. Only an activity its
own actor delivered is passed on (Arrival.Raw), so nothing forwarded is
forwarded again. The town checks it as relay.reply cells (specs/relay-five:
882 checks pass); Mastodon takes a passed-on activity only with an LD
signature, which GoToSocial and Akkoma don't add, and the checker knows it.
Events: a persona joins another server's event with a Join and leaves it with
a Leave, both to the organiser only, through
POST /api/privapub/v1/statuses/:id/join|leave; the organiser's Accept or
Reject is routed by our join id and shows as privapub.event.participation.
Events by invitation or taken on another site are refused before anything
is sent. Mobilizon's scenario joins and leaves an event (28 checks) and keeps
one for decePubClient's e2e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
274 lines
12 KiB
C#
274 lines
12 KiB
C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.ResponseCompression;
|
|
|
|
using OpenIddict.Validation.AspNetCore;
|
|
|
|
using PrivaPub.ClientModels;
|
|
using PrivaPub.Extensions;
|
|
using PrivaPub.Models;
|
|
using PrivaPub.Services;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.OpenApi;
|
|
using PrivaPub.Services.ClientToServer.Private;
|
|
using PrivaPub.Services.ClientToServer.Public;
|
|
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Federation.Outbox;
|
|
using PrivaPub.Federation.Signing;
|
|
using PrivaPub.Federation.Inbox;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Federation.Moderation;
|
|
using PrivaPub.Federation.Inbox.Handlers;
|
|
using PrivaPub.Domain.Content;
|
|
using PrivaPub.Domain.Relationships;
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Domain.Statuses;
|
|
using PrivaPub.Domain.Timelines;
|
|
using PrivaPub.Infrastructure.Http;
|
|
using PrivaPub.Infrastructure.Jobs;
|
|
using PrivaPub.Infrastructure.Geo;
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace PrivaPub.Middleware
|
|
{
|
|
public static class PrivaPubConfigurations
|
|
{
|
|
const string SchemeSelector = "PrivaPub";
|
|
|
|
public static IServiceCollection PrivaPubAppSettingsConfiguration(this IServiceCollection service, IConfiguration configuration)
|
|
{
|
|
return service
|
|
.Configure<MongoSettings>(configuration.GetSection(nameof(MongoSettings)))
|
|
.Configure<AppConfiguration>(configuration.GetSection(nameof(AppConfiguration)))
|
|
.Configure<PrivaPub.Infrastructure.RegistrationOptions>(configuration.GetSection("Registrations"));
|
|
}
|
|
public static IServiceCollection PrivaPubWorkersConfiguration(this IServiceCollection service)
|
|
{
|
|
return service;
|
|
//.AddHostedService<DiscussionsWorker>()
|
|
//.AddHostedService<GroupsCleanerWorker>()
|
|
//.AddHostedService<PoliciesCleanerWorker>();
|
|
}
|
|
public static IServiceCollection PrivaPubFederationConfiguration(this IServiceCollection service, IConfiguration configuration)
|
|
{
|
|
service.Configure<FederationOptions>(configuration.GetSection("Federation"));
|
|
service.AddHttpClient(FederationHttp.ClientName, (provider, client) =>
|
|
{
|
|
var baseAddress = provider.GetRequiredService<IOptionsMonitor<AppConfiguration>>().CurrentValue.BackendBaseAddress?.TrimEnd('/');
|
|
client.Timeout = FederationHttp.RequestTimeout;
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"PrivaPub/{BuildInfo.Ref} (+{baseAddress}/)");
|
|
})
|
|
.ConfigurePrimaryHttpMessageHandler(provider =>
|
|
SafeHttpHandlerFactory.Create(provider.GetRequiredService<IOptions<FederationOptions>>().Value, provider.GetRequiredService<IConnectedAddresses>()))
|
|
.AddHttpMessageHandler(provider => new EdgeHintsHandler(provider.GetRequiredService<IEdgeHints>()));
|
|
return service
|
|
.AddSingleton<IConnectedAddresses, ConnectedAddresses>()
|
|
.AddSingleton<IEdgeHints, EdgeHints>()
|
|
.AddSingleton<IFederationHttp, FederationHttp>()
|
|
.AddSingleton<IDomainBlocks, DomainBlocks>()
|
|
.AddSingleton<IContentRenderer, ContentRenderer>()
|
|
.AddSingleton<ILocalActorService, LocalActorService>()
|
|
.AddSingleton<IRemoteActorService, RemoteActorService>()
|
|
.AddSingleton<IDeliveryService, DeliveryService>()
|
|
.AddSingleton<IOutboxPublisher, OutboxPublisher>()
|
|
.AddSingleton<IGroupDistributor, GroupDistributor>()
|
|
.AddSingleton<ISignedFetchAuthorizer, SignedFetchAuthorizer>()
|
|
.AddSingleton<IFanout, Fanout>()
|
|
.AddSingleton<IInboxReceiver, InboxReceiver>()
|
|
.AddSingleton<IActivityHandler, FollowHandler>()
|
|
.AddSingleton<IActivityHandler, AcceptHandler>()
|
|
.AddSingleton<IActivityHandler, RejectHandler>()
|
|
.AddSingleton<IActivityHandler, UndoHandler>()
|
|
.AddSingleton<IActivityHandler, LikeHandler>()
|
|
.AddSingleton<IActivityHandler, DislikeHandler>()
|
|
.AddSingleton<IActivityHandler, EmojiReactHandler>()
|
|
.AddSingleton<IActivityHandler, QuoteRequestHandler>()
|
|
.AddSingleton<IReactions, Reactions>()
|
|
.AddSingleton<LinkPreviews>()
|
|
.AddSingleton<ILinkPreviews>(services => services.GetRequiredService<LinkPreviews>())
|
|
.AddSingleton<IJobHandler>(services => services.GetRequiredService<LinkPreviews>())
|
|
.AddSingleton<IActivityHandler, JoinHandler>()
|
|
.AddSingleton<IActivityHandler, AnnounceHandler>()
|
|
.AddSingleton<IActivityHandler, FlagHandler>()
|
|
.AddSingleton<IActivityHandler, BlockHandler>()
|
|
.AddSingleton<IActivityHandler, CreateHandler>()
|
|
.AddSingleton<IActivityHandler, DeleteHandler>()
|
|
.AddSingleton<IActivityHandler, UpdateHandler>()
|
|
.AddSingleton<IJobHandler, InboxProcessor>()
|
|
.AddSingleton<IRemotePosts, RemotePosts>()
|
|
.AddSingleton<IObjectRecords, ObjectRecords>()
|
|
.AddSingleton<IPollService, PollService>()
|
|
.AddSingleton<IQuoteService, QuoteService>()
|
|
.AddSingleton<IInteractionApprovals, InteractionApprovals>()
|
|
.AddSingleton<IParticipations, Participations>()
|
|
.AddSingleton<IJobHandler, PollRefreshJob>()
|
|
.AddSingleton<IJobHandler, RecoveryJob>()
|
|
.AddSingleton<IJobHandler, Federation.Actors.AccountCountsJob>()
|
|
.AddSingleton<IJobHandler, PollCloseJob>()
|
|
.AddSingleton<IJobHandler, Domain.Statuses.PublishScheduledJob>()
|
|
.AddSingleton<InstanceDescriber>()
|
|
.AddSingleton<IJobHandler>(services => services.GetRequiredService<InstanceDescriber>())
|
|
.AddSingleton<IJobHandler, AncestorsJobHandler>()
|
|
.AddSingleton<IJobHandler, Federation.Inbox.RepliesJobHandler>()
|
|
.AddSingleton<IJobQueue, JobQueue>()
|
|
.AddSingleton<IHostCircuitBreaker, HostCircuitBreaker>()
|
|
.AddSingleton<IJobHandler, DeliveryJobHandler>()
|
|
.AddHostedService<JobWorker>()
|
|
.AddHostedService<Domain.Social.FollowResender>();
|
|
}
|
|
public static IServiceCollection PrivaPubStatisticsConfiguration(this IServiceCollection service, IConfiguration configuration)
|
|
{
|
|
// not federation: the DB-IP download only (GeoUpdater), plain HTTPS to a fixed host
|
|
service.AddHttpClient(GeoUpdater.ClientName, (provider, client) =>
|
|
{
|
|
var baseAddress = provider.GetRequiredService<IOptionsMonitor<AppConfiguration>>().CurrentValue.BackendBaseAddress?.TrimEnd('/');
|
|
client.Timeout = TimeSpan.FromMinutes(15);
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"PrivaPub/{BuildInfo.Ref} (+{baseAddress}/)");
|
|
});
|
|
// not federation either: the CDNs' published address ranges (CdnUpdater), plain HTTPS to the CDNs' own hosts
|
|
service.AddHttpClient(CdnUpdater.ClientName, (provider, client) =>
|
|
{
|
|
var baseAddress = provider.GetRequiredService<IOptionsMonitor<AppConfiguration>>().CurrentValue.BackendBaseAddress?.TrimEnd('/');
|
|
client.Timeout = TimeSpan.FromMinutes(2);
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd($"PrivaPub/{BuildInfo.Ref} (+{baseAddress}/)");
|
|
});
|
|
return service
|
|
.Configure<StatisticsOptions>(configuration.GetSection("Statistics"))
|
|
.AddSingleton<InteractionSalts>()
|
|
.AddSingleton<InteractionLedger>()
|
|
.AddSingleton<IInteractionLedger>(services => services.GetRequiredService<InteractionLedger>())
|
|
.AddHostedService(services => services.GetRequiredService<InteractionLedger>())
|
|
.AddSingleton<IJobHandler, RollupJob>()
|
|
.AddSingleton<IGeoLocator, DbIpLocator>()
|
|
.AddSingleton<ICdnRanges, CdnRanges>()
|
|
.AddHostedService<CdnUpdater>()
|
|
.AddSingleton<Domain.Statistics.ISelfLocation, Domain.Statistics.SelfLocation>()
|
|
.AddSingleton<Domain.Statistics.IServerPlaces, Domain.Statistics.ServerPlaces>()
|
|
.AddHostedService<GeoUpdater>()
|
|
.AddSingleton<Domain.Statistics.StatisticsQueries>()
|
|
.AddSingleton<IJobHandler, Federation.Crawler.CrawlPlanner>()
|
|
.AddSingleton<IJobHandler, Federation.Crawler.InstanceCrawler>()
|
|
.AddHostedService<StatisticsSchedule>();
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubAuthServicesConfiguration(this IServiceCollection service, IConfiguration configuration)
|
|
{
|
|
return service
|
|
.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy(Policies.IsUser, Extensions.Extensions.IsUserPolicy());
|
|
options.AddPolicy(Policies.IsAdmin, Extensions.Extensions.IsAdminPolicy());
|
|
options.AddPolicy(Policies.IsModerator, Extensions.Extensions.IsModeratorPolicy());
|
|
})
|
|
.AddAuthentication(options =>
|
|
{
|
|
options.DefaultAuthenticateScheme = SchemeSelector;
|
|
options.DefaultChallengeScheme = SchemeSelector;
|
|
options.DefaultScheme = SchemeSelector;
|
|
})
|
|
.AddPolicyScheme(SchemeSelector, SchemeSelector, options => options.ForwardDefaultSelector = context =>
|
|
context.Request.Path.StartsWithSegments("/api")
|
|
? OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme
|
|
: JwtBearerDefaults.AuthenticationScheme)
|
|
.AddPrivaPubAuth(configuration)
|
|
.Services
|
|
.AddSingleton<AuthTokenManager>()
|
|
.AddSingleton<IPasswordHasher, PasswordHasher>();
|
|
}
|
|
public static IServiceCollection PrivaPubInternalizationConfiguration(this IServiceCollection service, IConfiguration configuration)
|
|
{
|
|
return service
|
|
.AddLocalization()
|
|
.AddSingleton<RequestLocalizationOptionsService>();
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubOptimizationConfiguration(this IServiceCollection service)
|
|
{
|
|
return service.AddResponseCompression(opts =>
|
|
{
|
|
opts.Providers.Add<BrotliCompressionProvider>();
|
|
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" });
|
|
});
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubDataBaseConfiguration(this IServiceCollection service)
|
|
{
|
|
return service.AddSingleton<DbEntities>();
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubServicesConfiguration(this IServiceCollection service)
|
|
{
|
|
return service
|
|
.AddTransient<IDataService, DataService>()
|
|
.AddTransient<IRootUsersService, RootUsersService>()
|
|
.AddScoped<IRootSessions, RootSessions>()
|
|
.AddScoped<IRootRemoval, RootRemoval>()
|
|
.AddTransient<IPublicAvatarUsersService, PublicAvatarUsersService>()
|
|
.AddTransient<IPrivateAvatarUsersService, PrivateAvatarUsersService>()
|
|
.AddTransient<IGroupUsersService, GroupUsersService>()
|
|
.AddTransient<IPostsService, PostsService>()
|
|
.AddTransient<IStatusService, StatusService>()
|
|
.AddTransient<IRelationshipService, RelationshipService>()
|
|
.AddTransient<IReportService, ReportService>()
|
|
.AddTransient<IFollowService, FollowService>()
|
|
.AddTransient<ITimelineService, TimelineService>()
|
|
.AddSingleton<AppConfigurationService>()
|
|
.AddHttpContextAccessor()
|
|
.AddMemoryCache()
|
|
.AddSingleton<IPasswordHasher, PasswordHasher>();
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubMiddlewareConfiguration(this IServiceCollection service)
|
|
{
|
|
return service
|
|
.AddEndpointsApiExplorer()
|
|
.AddSwaggerGen(c =>
|
|
{
|
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
{
|
|
In = ParameterLocation.Header,
|
|
Description = "Please enter a valid token",
|
|
Name = "Authorization",
|
|
Type = SecuritySchemeType.Http,
|
|
BearerFormat = "JWT",
|
|
Scheme = "bearer"
|
|
});
|
|
c.AddSecurityRequirement(document => new OpenApiSecurityRequirement
|
|
{
|
|
[new OpenApiSecuritySchemeReference("Bearer", document)] = []
|
|
});
|
|
})
|
|
.AddRazorPages(options => options.RootDirectory = "/Web/Pages")
|
|
.Services
|
|
.AddControllers(options => { options.Filters.Add<OperationCancelledExceptionFilter>(); })
|
|
.AddJsonOptions(options =>
|
|
{
|
|
options.JsonSerializerOptions.IgnoreReadOnlyFields = false;
|
|
options.JsonSerializerOptions.IgnoreReadOnlyProperties = false;
|
|
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
|
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
|
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
}).Services;
|
|
}
|
|
|
|
public static IServiceCollection PrivaPubCORSConfiguration(this IServiceCollection service)
|
|
{
|
|
return service.AddCors(options =>
|
|
{
|
|
options.DefaultPolicyName = "DefaultCORS";
|
|
options.AddDefaultPolicy(configure =>
|
|
{
|
|
configure.AllowAnyMethod()
|
|
.AllowAnyHeader()
|
|
.AllowAnyOrigin()
|
|
.WithExposedHeaders("Link", "X-RateLimit-Remaining", "X-RateLimit-Reset")
|
|
.DisallowCredentials();
|
|
});
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|