Votes out. A persona's downvote is a Dislike (POST /api/v1/statuses/:id/downvote and /undownvote, the viewer's own as privapub.votes.downvoted). One vote a post: a changed vote is sent as the new vote alone, as Lemmy sends one, since an Undo of the old one beside it could arrive after it and take the new one away; Undo goes only when a vote is taken back. A vote on a post in a remote community goes to the community, which counts it, and to the author only when on another server: one copy a server, since PieFed drops a second copy of an activity it has just seen. Mbin keeps a favourite apart from a vote, so there a favourite stays after a switch to a downvote. Feeds followed (owner decision 2026-10-07: through the server). Following a feed (Lemmy's multi-community, PieFed's feed) keeps a FeedSubscription for the persona and nothing else; the new Service privapub_feeds (LocalActorKind.Reader, reserved by migration _017) follows every community of the feeds read here, reconciled when a persona follows or leaves one, when a feed's list is read again (kept as it was when it cannot be read) and every six hours. Its Following rows carry FollowerKind, so nobody's home gets what it brings in and its unanswered follows are sent again as its own. The persona reads GET /api/v1/timelines/feed/:id (the feed's threads, ours included) and lists its feeds at GET /api/v1/feeds. Checked in the pasture, every scenario: 873 pass. The 14 failures are Hubzilla's (identical on the previous commit: Hubzilla no longer answers a follow in this pasture since its restore) and two activities Smithereen never sent; followsync passes once the pasture's restore is older than the 14-day pause. Live: alice's downvotes count as downvotes on Lemmy 1.0 and PieFed, replacing her upvote; Lemmy 1.0 and PieFed take privapub_feeds' follows and their threads reach the feed timelines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
298 lines
14 KiB
C#
298 lines
14 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, ReplyApprovalHandler>()
|
|
.AddSingleton<IActivityHandler, ReplyRejectionHandler>()
|
|
.AddSingleton<IActivityHandler, UndoHandler>()
|
|
.AddSingleton<IActivityHandler, LikeHandler>()
|
|
.AddSingleton<IActivityHandler, DislikeHandler>()
|
|
.AddSingleton<IActivityHandler, EmojiReactHandler>()
|
|
.AddSingleton<IActivityHandler, QuoteRequestHandler>()
|
|
.AddSingleton<IActivityHandler, WarnHandler>()
|
|
.AddSingleton<IActivityHandler, ResolveHandler>()
|
|
.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, LockHandler>()
|
|
.AddSingleton<Federation.Actors.IFeaturedPosts, Federation.Actors.FeaturedPosts>()
|
|
.AddSingleton<Federation.Actors.IWalls, Federation.Actors.Walls>()
|
|
.AddSingleton<IActivityHandler>(services => new FeaturedHandler(services.GetRequiredService<Federation.Actors.IFeaturedPosts>(), add: true,
|
|
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>(),
|
|
services.GetRequiredService<IJobQueue>()))
|
|
.AddSingleton<IActivityHandler>(services => new FeaturedHandler(services.GetRequiredService<Federation.Actors.IFeaturedPosts>(), add: false,
|
|
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>(),
|
|
services.GetRequiredService<IJobQueue>()))
|
|
.AddSingleton<IActivityHandler, MoveHandler>()
|
|
.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, Domain.Social.FeedSyncJob>()
|
|
.AddSingleton<IJobHandler, Federation.Actors.OutboxBackfill>()
|
|
.AddSingleton<IJobHandler, Federation.Actors.FollowingSynchronization>()
|
|
.AddSingleton<IJobHandler, PollCloseJob>()
|
|
.AddSingleton<IJobHandler, Domain.Media.ProcessMediaJob>()
|
|
.AddSingleton<IJobHandler, Domain.Portability.ExportArchiveJob>()
|
|
.AddSingleton<IJobHandler, Domain.Portability.ImportArchiveJob>()
|
|
.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>()
|
|
.AddHostedService<Domain.Social.FeedReader>()
|
|
.AddSingleton<Federation.Relays.IRelays, Federation.Relays.Relays>()
|
|
.AddHostedService<Federation.Relays.RelaySubscriber>();
|
|
}
|
|
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<IFeedFollows, FeedFollows>()
|
|
.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();
|
|
});
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|