[backend/asp] Make services runtime-discoverable

This commit is contained in:
Laura Hausmann 2024-11-17 04:20:27 +01:00
parent 705e061f74
commit 4356a47b9d
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
56 changed files with 255 additions and 213 deletions

View file

@ -1,9 +1,11 @@
using AngleSharp.Io;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.AspNetCore.Routing.Matching;
namespace Iceshrimp.Backend.Components.PublicPreview.Attributes;
public class PublicPreviewRouteMatcher : MatcherPolicy, IEndpointSelectorPolicy
public class PublicPreviewRouteMatcher : MatcherPolicy, IEndpointSelectorPolicy, ISingletonService,
IService<MatcherPolicy>
{
public override int Order => 99999; // That's ActionConstraintMatcherPolicy - 1

View file

@ -1,5 +1,6 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Parsing;
using Microsoft.AspNetCore.Components;
@ -7,7 +8,7 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Components.PublicPreview.Renderers;
public class MfmRenderer(IOptions<Config.InstanceSection> config)
public class MfmRenderer(IOptions<Config.InstanceSection> config) : ISingletonService
{
private readonly MfmConverter _converter = new(config);

View file

@ -14,7 +14,7 @@ public class NoteRenderer(
MfmRenderer mfm,
IOptions<Config.InstanceSection> instance,
IOptionsSnapshot<Config.SecuritySection> security
)
) : IScopedService
{
public async Task<PreviewNote?> RenderOne(Note? note)
{

View file

@ -13,7 +13,7 @@ public class UserRenderer(
MfmRenderer mfm,
IOptions<Config.InstanceSection> instance,
IOptionsSnapshot<Config.SecuritySection> security
)
) : IScopedService
{
public async Task<PreviewUser?> RenderOne(User? user)
{

View file

@ -29,7 +29,7 @@ public class ActivityPubController(
ActivityPub.NoteRenderer noteRenderer,
ActivityPub.UserRenderer userRenderer,
IOptions<Config.InstanceSection> config
) : ControllerBase
) : ControllerBase, IScopedService
{
[HttpGet("/notes/{id}")]
[AuthorizedFetch]

View file

@ -20,7 +20,7 @@ public class NoteRenderer(
MfmConverter mfmConverter,
DatabaseContext db,
EmojiService emojiSvc
)
) : IScopedService
{
private static readonly FilterResultEntity InaccessibleFilter = new()
{

View file

@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer, UserRenderer userRenderer)
public class NotificationRenderer(DatabaseContext db, NoteRenderer noteRenderer, UserRenderer userRenderer) : IScopedService
{
public async Task<NotificationEntity> RenderAsync(
Notification notification, User user, bool isPleroma, List<AccountEntity>? accounts = null,

View file

@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Mastodon.Renderers;
public class PollRenderer(DatabaseContext db)
public class PollRenderer(DatabaseContext db) : IScopedService
{
public async Task<PollEntity> RenderAsync(Poll poll, User? user, PollRendererDto? data = null)
{

View file

@ -14,7 +14,7 @@ public class UserRenderer(
IOptionsSnapshot<Config.SecuritySection> security,
MfmConverter mfmConverter,
DatabaseContext db
)
) : IScopedService
{
private readonly string _transparent = $"https://{config.Value.WebDomain}/assets/transparent.png";

View file

@ -15,7 +15,7 @@ public class NoteRenderer(
DatabaseContext db,
EmojiService emojiSvc,
IOptions<Config.InstanceSection> config
)
) : IScopedService
{
public async Task<NoteResponse> RenderOne(
Note note, User? user, Filter.FilterContext? filterContext = null, NoteRendererDto? data = null

View file

@ -5,7 +5,7 @@ using static Iceshrimp.Shared.Schemas.Web.NotificationResponse;
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer)
public class NotificationRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer) : IScopedService
{
private static NotificationResponse Render(Notification notification, NotificationRendererDto data)
{

View file

@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
public class UserProfileRenderer(DatabaseContext db)
public class UserProfileRenderer(DatabaseContext db) : IScopedService
{
public async Task<UserProfileResponse> RenderOne(User user, User? localUser, UserRendererDto? data = null)
{

View file

@ -1,13 +1,14 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Shared.Schemas.Web;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db)
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) : IScopedService
{
private UserResponse Render(User user, UserRendererDto data)
{

View file

@ -3,19 +3,10 @@ using System.Reflection;
using System.Threading.RateLimiting;
using System.Xml.Linq;
using Iceshrimp.AssemblyUtils;
using Iceshrimp.Backend.Components.PublicPreview.Attributes;
using Iceshrimp.Backend.Components.PublicPreview.Renderers;
using Iceshrimp.Backend.Controllers.Federation;
using Iceshrimp.Backend.Controllers.Mastodon.Renderers;
using Iceshrimp.Backend.Controllers.Web.Renderers;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Federation.WebFinger;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Backend.Core.Services.ImageProcessing;
using Iceshrimp.Backend.SignalR.Authentication;
using Iceshrimp.Shared.Configuration;
using Iceshrimp.Shared.Schemas.Web;
@ -33,9 +24,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using NoteRenderer = Iceshrimp.Backend.Controllers.Web.Renderers.NoteRenderer;
using NotificationRenderer = Iceshrimp.Backend.Controllers.Web.Renderers.NotificationRenderer;
using UserRenderer = Iceshrimp.Backend.Controllers.Web.Renderers.UserRenderer;
namespace Iceshrimp.Backend.Core.Extensions;
@ -43,91 +31,40 @@ public static class ServiceExtensions
{
public static void AddServices(this IServiceCollection services, IConfiguration configuration)
{
// Transient = instantiated per request and class
var config = configuration.Get<Config>() ?? throw new Exception("Failed to read storage config section");
// Scoped = instantiated per request
services
.AddScoped<ActivityPub.ActivityRenderer>()
.AddScoped<ActivityPub.UserRenderer>()
.AddScoped<ActivityPub.NoteRenderer>()
.AddScoped<ActivityPub.UserResolver>()
.AddScoped<ActivityPub.ObjectResolver>()
.AddScoped<ActivityPub.MentionsResolver>()
.AddScoped<ActivityPub.ActivityDeliverService>()
.AddScoped<ActivityPub.FederationControlService>()
.AddScoped<ActivityPub.ActivityHandlerService>()
.AddScoped<ActivityPub.ActivityFetcherService>()
.AddScoped<UserService>()
.AddScoped<NoteService>()
.AddScoped<EmojiService>()
.AddScoped<EmojiImportService>()
.AddScoped<WebFingerService>()
.AddScoped<SystemUserService>()
.AddScoped<DriveService>()
.AddScoped<NotificationService>()
.AddScoped<DatabaseMaintenanceService>()
.AddScoped<BiteService>()
.AddScoped<ImportExportService>()
.AddScoped<UserProfileMentionsResolver>()
.AddScoped<Controllers.Mastodon.Renderers.UserRenderer>()
.AddScoped<Controllers.Mastodon.Renderers.NoteRenderer>()
.AddScoped<Controllers.Mastodon.Renderers.NotificationRenderer>()
.AddScoped<PollRenderer>()
.AddScoped<PollService>()
.AddScoped<NoteRenderer>()
.AddScoped<UserRenderer>()
.AddScoped<NotificationRenderer>()
.AddScoped<ActivityPubController>()
.AddScoped<FollowupTaskService>()
.AddScoped<InstanceService>()
.AddScoped<MfmConverter>()
.AddScoped<UserProfileRenderer>()
.AddScoped<CacheService>()
.AddScoped<MetaService>()
.AddScoped<StorageMaintenanceService>()
.AddScoped<RelayService>()
.AddScoped<Components.PublicPreview.Renderers.UserRenderer>()
.AddScoped<Components.PublicPreview.Renderers.NoteRenderer>();
var serviceTypes = PluginLoader
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<IService>)
.OrderBy(type => type.GetInterfaceProperty<IService, int?>(nameof(IService.Priority)) ?? 0)
.ToArray();
// Singleton = instantiated once across application lifetime
services
.AddSingleton<HttpClient, CustomHttpClient>()
.AddSingleton<HttpRequestService>()
.AddSingleton<CronService>()
.AddSingleton<QueueService>()
.AddSingleton<ObjectStorageService>()
.AddSingleton<EventService>()
.AddSingleton<PushService>()
.AddSingleton<StreamingService>()
.AddSingleton<ImageProcessor>()
.AddSingleton<RazorViewRenderService>()
.AddSingleton<MfmRenderer>()
.AddSingleton<MatcherPolicy, PublicPreviewRouteMatcher>()
.AddSingleton<PolicyService>();
var config = configuration.GetSection("Storage").Get<Config.StorageSection>() ??
throw new Exception("Failed to read storage config section");
switch (config.MediaProcessing.ImageProcessor)
foreach (var type in serviceTypes)
{
case Enums.ImageProcessor.LibVips:
services.AddSingleton<IImageProcessor, VipsProcessor>();
services.AddSingleton<IImageProcessor, ImageSharpProcessor>();
break;
case Enums.ImageProcessor.ImageSharp:
services.AddSingleton<IImageProcessor, ImageSharpProcessor>();
break;
case Enums.ImageProcessor.None:
break;
default:
throw new ArgumentOutOfRangeException();
if (type.GetInterfaceProperty<IService, ServiceLifetime?>(nameof(IService.Lifetime)) is not { } lifetime)
continue;
if (type.GetInterface(nameof(IConditionalService)) != null)
if (type.CallInterfaceMethod(nameof(IConditionalService.Predicate), config) is not true)
continue;
var serviceType = type.GetInterfaceProperty<IService, Type>(nameof(IService.ServiceType)) ?? type;
services.Add(new ServiceDescriptor(serviceType, type, lifetime));
}
// Hosted services = long running background tasks
// Note: These need to be added as a singleton as well to ensure data consistency
services.AddHostedService<CronService>(provider => provider.GetRequiredService<CronService>());
services.AddHostedService<QueueService>(provider => provider.GetRequiredService<QueueService>());
services.AddHostedService<PushService>(provider => provider.GetRequiredService<PushService>());
var hostedServiceTypes = PluginLoader
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<IHostedService>)
.ToArray();
foreach (var type in hostedServiceTypes)
{
if (type.GetInterface(nameof(IService)) == null)
services.Add(new ServiceDescriptor(type, type, ServiceLifetime.Singleton));
services.Add(new ServiceDescriptor(typeof(IHostedService), provider => provider.GetRequiredService(type),
ServiceLifetime.Singleton));
}
}
public static void AddMiddleware(this IServiceCollection services)
@ -413,6 +350,51 @@ public static partial class HttpContextExtensions
ctx.Items.TryGetValue(CacheKey, out var s) && s is true;
}
public interface IService
{
// This should be abstract instead of virtual but the runtime team said https://github.com/dotnet/runtime/issues/79331
public static virtual ServiceLifetime Lifetime => throw new Exception("Missing IService.Lifetime override");
public static virtual Type? ServiceType => null;
public static virtual int Priority => 0;
}
/// <summary>
/// Instantiated per request and class
/// </summary>
public interface ITransientService : IService
{
static ServiceLifetime IService.Lifetime => ServiceLifetime.Transient;
}
/// <summary>
/// Instantiated per request
/// </summary>
public interface IScopedService : IService
{
static ServiceLifetime IService.Lifetime => ServiceLifetime.Scoped;
}
/// <summary>
/// Instantiated once across application lifetime
/// </summary>
public interface ISingletonService : IService
{
static ServiceLifetime IService.Lifetime => ServiceLifetime.Singleton;
}
public interface IService<TService> : IService
{
static Type IService.ServiceType => typeof(TService);
}
public interface IConditionalService : IService
{
// This should be abstract instead of virtual but the runtime team said https://github.com/dotnet/runtime/issues/79331
public static virtual bool Predicate(Config ctx) =>
throw new Exception("Missing IConditionalService.Predicate override");
}
#region AsyncDataProtection handlers
/// <summary>

View file

@ -1,5 +1,6 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Queues;
@ -13,7 +14,7 @@ public class ActivityDeliverService(
ILogger<ActivityDeliverService> logger,
QueueService queueService,
DatabaseContext db
)
) : IScopedService
{
public async Task DeliverToFollowersAsync(ASActivity activity, User actor, IEnumerable<User> recipients)
{

View file

@ -23,7 +23,7 @@ public class ActivityFetcherService(
DatabaseContext db,
ILogger<ActivityFetcherService> logger,
FederationControlService fedCtrlSvc
)
) : IScopedService
{
private static readonly IReadOnlyCollection<string> AcceptableActivityTypes =
[

View file

@ -29,7 +29,7 @@ public class ActivityHandlerService(
EmojiService emojiSvc,
EventService eventSvc,
RelayService relaySvc
)
) : IScopedService
{
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
{

View file

@ -12,7 +12,7 @@ public class ActivityRenderer(
IOptions<Config.InstanceSection> config,
UserRenderer userRenderer,
NoteRenderer noteRenderer
)
) : IScopedService
{
private string GenerateActivityId() =>
$"https://{config.Value.WebDomain}/activities/ephemeral/{Guid.NewGuid().ToStringLower()}";

View file

@ -13,7 +13,7 @@ public class FederationControlService(
IOptionsSnapshot<Config.SecuritySection> options,
IOptions<Config.InstanceSection> instance,
DatabaseContext db
)
) : IScopedService
{
//TODO: we need some level of caching here
public async Task<bool> ShouldBlockAsync(params string?[] hosts)

View file

@ -16,9 +16,7 @@ using SplitDomainMapping = IReadOnlyDictionary<(string usernameLower, string web
/// Resolves mentions into their canonical form. This is required for handling split domain mentions correctly, as it
/// cannot be guaranteed that remote instances handle split domain users correctly.
/// </summary>
public class MentionsResolver(
IOptions<Config.InstanceSection> config
)
public class MentionsResolver(IOptions<Config.InstanceSection> config) : ISingletonService
{
public string ResolveMentions(
string mfm, string? host,

View file

@ -10,7 +10,11 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter mfmConverter, DatabaseContext db)
public class NoteRenderer(
IOptions<Config.InstanceSection> config,
MfmConverter mfmConverter,
DatabaseContext db
) : IScopedService
{
/// <summary>
/// This function is meant for compacting a note into the @id form as specified in ActivityStreams

View file

@ -1,6 +1,7 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.EntityFrameworkCore;
@ -14,7 +15,7 @@ public class ObjectResolver(
DatabaseContext db,
FederationControlService federationCtrl,
IOptions<Config.InstanceSection> config
)
) : IScopedService
{
public async Task<ASObject?> ResolveObject(
ASObjectBase baseObj, string? actorUri = null, int recurse = 5, bool force = false, User? user = null

View file

@ -2,6 +2,7 @@ using AngleSharp.Text;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Microsoft.EntityFrameworkCore;
@ -9,7 +10,11 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db, MfmConverter mfmConverter)
public class UserRenderer(
IOptions<Config.InstanceSection> config,
DatabaseContext db,
MfmConverter mfmConverter
) : IScopedService
{
/// <summary>
/// This function is meant for compacting an actor into the @id form as specified in ActivityStreams

View file

@ -3,6 +3,7 @@ using AsyncKeyedLock;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.WebFinger;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
@ -20,7 +21,7 @@ public class UserResolver(
ActivityFetcherService fetchSvc,
IOptions<Config.InstanceSection> config,
DatabaseContext db
)
) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -4,6 +4,7 @@ using System.Text.Encodings.Web;
using System.Xml;
using System.Xml.Serialization;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services;
using Microsoft.Extensions.Options;
@ -27,7 +28,7 @@ public class WebFingerService(
HttpRequestService httpRqSvc,
IHostApplicationLifetime appLifetime,
IOptions<Config.InstanceSection> config
)
) : ISingletonService
{
private static readonly ImmutableArray<string> Accept =
[

View file

@ -16,7 +16,9 @@ using HtmlParser = AngleSharp.Html.Parser.HtmlParser;
namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
public class MfmConverter(IOptions<Config.InstanceSection> config)
public class MfmConverter(
IOptions<Config.InstanceSection> config
) : IScopedService // <- this is intentional, see property below
{
public bool SupportsHtmlFormatting { private get; set; } = true;

View file

@ -1,12 +1,19 @@
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services;
public class BiteService(DatabaseContext db, ActivityPub.ActivityRenderer activityRenderer, ActivityPub.ActivityDeliverService deliverSvc, NotificationService notificationSvc, IOptions<Config.InstanceSection> config)
public class BiteService(
DatabaseContext db,
ActivityPub.ActivityRenderer activityRenderer,
ActivityPub.ActivityDeliverService deliverSvc,
NotificationService notificationSvc,
IOptions<Config.InstanceSection> config
) : IScopedService
{
public async Task BiteAsync(User user, Bite target)
{
@ -24,7 +31,8 @@ public class BiteService(DatabaseContext db, ActivityPub.ActivityRenderer activi
if (target.UserHost != null)
{
var activity = activityRenderer.RenderBite(bite, target.Uri ?? target.GetPublicUri(config.Value), target.User);
var activity =
activityRenderer.RenderBite(bite, target.Uri ?? target.GetPublicUri(config.Value), target.User);
await deliverSvc.DeliverToAsync(activity, user, target.User);
}
@ -47,7 +55,8 @@ public class BiteService(DatabaseContext db, ActivityPub.ActivityRenderer activi
if (target.UserHost != null)
{
var activity = activityRenderer.RenderBite(bite, target.Uri ?? target.GetPublicUri(config.Value), target.User);
var activity =
activityRenderer.RenderBite(bite, target.Uri ?? target.GetPublicUri(config.Value), target.User);
await deliverSvc.DeliverToAsync(activity, user, target.User);
}

View file

@ -3,12 +3,12 @@ using System.Text.Json.Serialization;
using AsyncKeyedLock;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
//TODO: named caches (with prefix)
public class CacheService([FromKeyedServices("cache")] DatabaseContext db)
public class CacheService([FromKeyedServices("cache")] DatabaseContext db) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -7,7 +7,7 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services;
public class CustomHttpClient : HttpClient
public class CustomHttpClient : HttpClient, IService<HttpClient>, ISingletonService
{
private static readonly FastFallback FastFallbackHandler = new();

View file

@ -1,9 +1,10 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class DatabaseMaintenanceService(DatabaseContext db)
public class DatabaseMaintenanceService(DatabaseContext db) : IScopedService
{
public async Task RecomputeNoteCountersAsync()
{

View file

@ -27,7 +27,7 @@ public class DriveService(
QueueService queueSvc,
ILogger<DriveService> logger,
ImageProcessor imageProcessor
)
) : IScopedService
{
public async Task<DriveFile?> StoreFile(
string? uri, User user, bool sensitive, string? description = null, string? mimeType = null,

View file

@ -1,6 +1,7 @@
using System.IO.Compression;
using System.Net;
using System.Text.Json;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.AspNetCore.StaticFiles;
@ -30,7 +31,7 @@ public record EmojiZip(EmojiZipMeta Metadata, ZipArchive Archive);
public class EmojiImportService(
EmojiService emojiSvc,
ILogger<EmojiImportService> logger
)
) : IScopedService
{
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);

View file

@ -18,7 +18,7 @@ public partial class EmojiService(
DriveService driveSvc,
SystemUserService sysUserSvc,
IOptions<Config.InstanceSection> config
)
) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -1,9 +1,10 @@
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Events;
using Iceshrimp.Backend.Core.Extensions;
namespace Iceshrimp.Backend.Core.Services;
public class EventService
public class EventService : ISingletonService
{
public event EventHandler<Note>? NotePublished;
public event EventHandler<Note>? NoteUpdated;

View file

@ -1,6 +1,11 @@
using Iceshrimp.Backend.Core.Extensions;
namespace Iceshrimp.Backend.Core.Services;
public class FollowupTaskService(IServiceScopeFactory serviceScopeFactory, ILogger<FollowupTaskService> logger)
public class FollowupTaskService(
IServiceScopeFactory serviceScopeFactory,
ILogger<FollowupTaskService> logger
) : IScopedService
{
public bool IsBackgroundWorker { get; private set; }

View file

@ -3,12 +3,13 @@ using System.Net.Http.Headers;
using System.Security.Cryptography;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.Cryptography;
using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services;
public class HttpRequestService(IOptions<Config.InstanceSection> options)
public class HttpRequestService(IOptions<Config.InstanceSection> options) : ISingletonService
{
private static HttpRequestMessage GenerateRequest(
string url, HttpMethod method,

View file

@ -2,13 +2,14 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Microsoft.Extensions.Options;
using static Iceshrimp.Backend.Core.Services.ImageProcessing.ImageVersion;
namespace Iceshrimp.Backend.Core.Services.ImageProcessing;
public class ImageProcessor
public class ImageProcessor : ISingletonService
{
private readonly IOptionsMonitor<Config.StorageSection> _config;
//TODO: support stripping of exif/icc metadata (without re-encoding)

View file

@ -1,5 +1,6 @@
using CommunityToolkit.HighPerformance;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp;
@ -13,7 +14,8 @@ using ImageSharpConfig = SixLabors.ImageSharp.Configuration;
namespace Iceshrimp.Backend.Core.Services.ImageProcessing;
public class ImageSharpProcessor : ImageProcessorBase, IImageProcessor
public class ImageSharpProcessor : ImageProcessorBase, IImageProcessor,
ISingletonService, IConditionalService, IService<IImageProcessor>
{
private readonly ILogger<ImageSharpProcessor> _logger;
private readonly ImageSharpConfig _sharpConfig;
@ -22,6 +24,9 @@ public class ImageSharpProcessor : ImageProcessorBase, IImageProcessor
public bool CanIdentify => true;
public bool CanGenerateBlurhash => true;
public static bool Predicate(Config ctx) =>
ctx.Storage.MediaProcessing.ImageProcessor is Enums.ImageProcessor.ImageSharp or Enums.ImageProcessor.LibVips;
public ImageSharpProcessor(
ILogger<ImageSharpProcessor> logger, IOptions<Config.StorageSection> config
) : base("ImageSharp", 1)

View file

@ -1,19 +1,28 @@
using System.Runtime.InteropServices;
using CommunityToolkit.HighPerformance;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using NetVips;
using Iceshrimp.MimeTypes;
using SixLabors.ImageSharp.PixelFormats;
using Enums = NetVips.Enums;
namespace Iceshrimp.Backend.Core.Services.ImageProcessing;
public class VipsProcessor : ImageProcessorBase, IImageProcessor
public class VipsProcessor : ImageProcessorBase, IImageProcessor,
ISingletonService, IConditionalService, IService<IImageProcessor>
{
private readonly ILogger<VipsProcessor> _logger;
public bool CanIdentify => true;
public bool CanGenerateBlurhash => true;
static int IService.Priority => -1;
public static bool Predicate(Config ctx) =>
ctx.Storage.MediaProcessing.ImageProcessor == Configuration.Enums.ImageProcessor.LibVips;
public VipsProcessor(ILogger<VipsProcessor> logger) : base("LibVips", 0)
{
_logger = logger;

View file

@ -15,7 +15,7 @@ public class ImportExportService(
CacheService cacheSvc,
UserService userSvc,
ActivityPub.UserResolver userResolver
)
) : IScopedService
{
public async Task<string> ExportFollowingAsync(User user)
{

View file

@ -8,7 +8,11 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class InstanceService(DatabaseContext db, HttpClient httpClient, ILogger<InstanceService> logger)
public class InstanceService(
DatabaseContext db,
HttpClient httpClient,
ILogger<InstanceService> logger
) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -1,10 +1,11 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class MetaService([FromKeyedServices("cache")] DatabaseContext db)
public class MetaService([FromKeyedServices("cache")] DatabaseContext db) : IScopedService
{
public async Task<T> Get<T>(Meta<T> meta) => meta.ConvertGet(await Fetch(meta.Key));

View file

@ -19,8 +19,6 @@ using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Core.Services;
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor",
Justification = "We need IOptionsSnapshot for config hot reload")]
public class NoteService(
ILogger<NoteService> logger,
DatabaseContext db,
@ -43,7 +41,7 @@ public class NoteService(
PollService pollSvc,
ActivityPub.FederationControlService fedCtrlSvc,
PolicyService policySvc
)
) : IScopedService
{
private const int DefaultRecursionLimit = 100;

View file

@ -8,10 +8,9 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class NotificationService(
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
DatabaseContext db,
EventService eventSvc
)
) : IScopedService
{
public async Task GenerateMentionNotifications(Note note, IReadOnlyCollection<string> mentionedLocalUserIds)
{

View file

@ -3,6 +3,7 @@ using System.Net.Http.Headers;
using System.Text;
using Carbon.Storage;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.ObjectStorage.Core.Models;
using Iceshrimp.ObjectStorage.Core.Security;
@ -11,7 +12,7 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Core.Services;
public class ObjectStorageService(IOptions<Config.StorageSection> config, HttpClient httpClient)
public class ObjectStorageService(IOptions<Config.StorageSection> config, HttpClient httpClient) : ISingletonService
{
private readonly string? _accessUrl = config.Value.ObjectStorage?.AccessUrl;

View file

@ -4,6 +4,7 @@ using System.Text.Json;
using Iceshrimp.AssemblyUtils;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Shared.Configuration;
@ -11,7 +12,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class PolicyService(IServiceScopeFactory scopeFactory)
public class PolicyService(IServiceScopeFactory scopeFactory) : ISingletonService
{
private bool _initialized;
private IRejectPolicy[] _rejectPolicies = [];

View file

@ -1,5 +1,6 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
@ -9,7 +10,7 @@ public class PollService(
ActivityPub.ActivityRenderer activityRenderer,
ActivityPub.UserRenderer userRenderer,
ActivityPub.ActivityDeliverService deliverSvc
)
) : IScopedService
{
public async Task RegisterPollVote(PollVote pollVote, Poll poll, Note note, bool updateVotersCount = true)
{

View file

@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text.Encodings.Web;
using Iceshrimp.Backend.Core.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -15,7 +16,7 @@ public class RazorViewRenderService(
ITempDataProvider tempDataProvider,
IHttpContextAccessor httpContextAccessor,
IRazorPageActivator activator
)
) : ISingletonService
{
private async Task RenderAsync<T>(string path, T model, TextWriter writer) where T : PageModel
{

View file

@ -1,5 +1,6 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.EntityFrameworkCore;
@ -12,7 +13,7 @@ public class RelayService(
ActivityPub.ActivityRenderer activityRenderer,
ActivityPub.ActivityDeliverService deliverSvc,
ActivityPub.UserRenderer userRenderer
)
) : IScopedService
{
public async Task SubscribeToRelay(string uri)
{

View file

@ -16,7 +16,7 @@ public class StorageMaintenanceService(
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
IOptionsSnapshot<Config.StorageSection> options,
ILogger<StorageMaintenanceService> logger
)
) : IScopedService
{
public async Task MigrateLocalFiles(bool purge)
{

View file

@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using Iceshrimp.Backend.Controllers.Web.Renderers;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.SignalR;
using Iceshrimp.Backend.SignalR.Helpers;
using Iceshrimp.Shared.Schemas.SignalR;
@ -9,7 +10,7 @@ using Microsoft.AspNetCore.SignalR;
namespace Iceshrimp.Backend.Core.Services;
public sealed class StreamingService
public sealed class StreamingService : ISingletonService
{
private readonly ConcurrentDictionary<string, StreamingConnectionAggregate> _connections = [];
private readonly EventService _eventSvc;

View file

@ -2,12 +2,13 @@ using System.Security.Cryptography;
using AsyncKeyedLock;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Helpers;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
public class SystemUserService(ILogger<SystemUserService> logger, DatabaseContext db)
public class SystemUserService(ILogger<SystemUserService> logger, DatabaseContext db) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -14,7 +14,10 @@ namespace Iceshrimp.Backend.Core.Services;
using MentionTuple = (List<Note.MentionedUser> mentions,
Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping);
public class UserProfileMentionsResolver(ActivityPub.UserResolver userResolver, IOptions<Config.InstanceSection> config)
public class UserProfileMentionsResolver(
ActivityPub.UserResolver userResolver,
IOptions<Config.InstanceSection> config
) : IScopedService
{
public async Task<MentionTuple> ResolveMentions(ASActor actor, string? host)
{

View file

@ -22,7 +22,6 @@ using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Backend.Core.Services;
public class UserService(
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
IOptionsSnapshot<Config.SecuritySection> security,
IOptions<Config.InstanceSection> instance,
ILogger<UserService> logger,
@ -40,7 +39,7 @@ public class UserService(
EventService eventSvc,
WebFingerService webFingerSvc,
ActivityPub.FederationControlService fedCtrlSvc
)
) : IScopedService
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
{

View file

@ -47,7 +47,7 @@
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Ulid" Version="1.3.4" />
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.2" />
<PackageReference Include="Iceshrimp.AssemblyUtils" Version="1.0.3" />
<PackageReference Include="Iceshrimp.MimeTypes" Version="1.0.1" />
<PackageReference Include="Iceshrimp.WebPush" Version="2.1.0" />
<PackageReference Include="NetVips" Version="3.0.0" />

View file

@ -38,7 +38,6 @@ builder.Services.AddRazorPages();
builder.Services.AddRazorComponents();
builder.Services.AddAntiforgery(o => o.Cookie.Name = "CSRF-Token");
builder.Services.AddMiddleware();
builder.Services.AddServices(builder.Configuration);
builder.Services.ConfigureServices(builder.Configuration);