diff --git a/Iceshrimp.Backend/Controllers/Web/AdminController.cs b/Iceshrimp.Backend/Controllers/Web/AdminController.cs index 7e19bfd8..f87bd63a 100644 --- a/Iceshrimp.Backend/Controllers/Web/AdminController.cs +++ b/Iceshrimp.Backend/Controllers/Web/AdminController.cs @@ -12,7 +12,6 @@ using Iceshrimp.Backend.Core.Federation.ActivityStreams; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Middleware; -using Iceshrimp.Backend.Core.Policies; using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Tasks; using Iceshrimp.Shared.Schemas.Web; @@ -20,6 +19,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; +using static Iceshrimp.Backend.Core.Extensions.SwaggerGenOptionsExtensions; namespace Iceshrimp.Backend.Controllers.Web; @@ -187,87 +187,34 @@ public class AdminController( await new MediaCleanupTask().Invoke(scope.ServiceProvider); } - [HttpGet("policy/word-reject")] + [HttpGet("policy")] [ProducesResults(HttpStatusCode.OK)] - public async Task GetWordRejectPolicy() - { - var raw = await db.PolicyConfiguration.Where(p => p.Name == nameof(WordRejectPolicy)) - .Select(p => p.Data) - .FirstOrDefaultAsync(); + public async Task> GetAvailablePolicies() => await policySvc.GetAvailablePolicies(); - var res = raw != null ? JsonSerializer.Deserialize(raw) : null; - res ??= new WordRejectPolicyConfiguration { Enabled = false, Words = [] }; - return res; + [HttpGet("policy/{name}")] + [ProducesResults(HttpStatusCode.OK)] + [ProducesErrors(HttpStatusCode.NotFound)] + public async Task GetPolicyConfiguration(string name) + { + var raw = await db.PolicyConfiguration.Where(p => p.Name == name).Select(p => p.Data).FirstOrDefaultAsync(); + return await policySvc.GetConfiguration(name, raw) ?? throw GracefulException.NotFound("Policy not found"); } - [HttpGet("policy/user-age-reject")] + [HttpPut("policy/{name}")] [ProducesResults(HttpStatusCode.OK)] - public async Task GetUserAgeRejectPolicy() + [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)] + public async Task UpdateWordRejectPolicy( + string name, [SwaggerBodyExample("{\n \"enabled\": true\n}")] JsonDocument body + ) { - var raw = await db.PolicyConfiguration.Where(p => p.Name == nameof(UserAgeRejectPolicy)) - .Select(p => p.Data) - .FirstOrDefaultAsync(); + // @formatter:off + var type = await policySvc.GetConfigurationType(name) ?? throw GracefulException.NotFound("Policy not found"); + var data = body.Deserialize(type) as IPolicyConfiguration ?? throw GracefulException.BadRequest("Invalid policy config"); + if (data.GetType() != type) throw GracefulException.BadRequest("Invalid policy config"); + // @formatter:on - var res = raw != null ? JsonSerializer.Deserialize(raw) : null; - res ??= new UserAgeRejectPolicyConfiguration { Enabled = false, Age = TimeSpan.Zero }; - return res; - } - - [HttpGet("policy/hellthread-reject")] - [ProducesResults(HttpStatusCode.OK)] - public async Task GetHellthreadRejectPolicy() - { - var raw = await db.PolicyConfiguration.Where(p => p.Name == nameof(HellthreadRejectPolicy)) - .Select(p => p.Data) - .FirstOrDefaultAsync(); - - var res = raw != null ? JsonSerializer.Deserialize(raw) : null; - res ??= new HellthreadRejectPolicyConfiguration { Enabled = false, MentionLimit = 0 }; - return res; - } - - [HttpPut("policy/word-reject")] - [ProducesResults(HttpStatusCode.OK)] - [ProducesErrors(HttpStatusCode.BadRequest)] - public async Task UpdateWordRejectPolicy(WordRejectPolicyConfiguration data) - { await db.PolicyConfiguration - .Upsert(new PolicyConfiguration - { - Name = nameof(WordRejectPolicy), Data = JsonSerializer.Serialize(data) - }) - .On(p => new { p.Name }) - .RunAsync(); - - await policySvc.Update(); - } - - [HttpPut("policy/user-age-reject")] - [ProducesResults(HttpStatusCode.OK)] - [ProducesErrors(HttpStatusCode.BadRequest)] - public async Task UpdateUserAgeRejectPolicy(UserAgeRejectPolicyConfiguration data) - { - await db.PolicyConfiguration - .Upsert(new PolicyConfiguration - { - Name = nameof(UserAgeRejectPolicy), Data = JsonSerializer.Serialize(data) - }) - .On(p => new { p.Name }) - .RunAsync(); - - await policySvc.Update(); - } - - [HttpPut("policy/hellthread-reject")] - [ProducesResults(HttpStatusCode.OK)] - [ProducesErrors(HttpStatusCode.BadRequest)] - public async Task UpdateHellthreadRejectPolicy(HellthreadRejectPolicyConfiguration data) - { - await db.PolicyConfiguration.Upsert(new PolicyConfiguration - { - Name = nameof(HellthreadRejectPolicy), - Data = JsonSerializer.Serialize(data) - }) + .Upsert(new PolicyConfiguration { Name = name, Data = JsonSerializer.Serialize(data) }) .On(p => new { p.Name }) .RunAsync(); diff --git a/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs b/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs index 35b8dae6..9302fce9 100644 --- a/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/SwaggerGenOptionsExtensions.cs @@ -19,6 +19,7 @@ public static class SwaggerGenOptionsExtensions public static void AddFilters(this SwaggerGenOptions options) { options.SchemaFilter(); + options.SchemaFilter(); options.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately. options.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable options.UseAllOfForInheritance(); // Allows $ref objects to be nullable @@ -68,6 +69,18 @@ public static class SwaggerGenOptionsExtensions } } + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", + Justification = "SwaggerGenOptions.SchemaFilter instantiates this class at runtime")] + private class SwaggerBodyExampleSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var att = context.ParameterInfo?.GetCustomAttribute(); + if (att != null) + schema.Example = new OpenApiString(att.Value); + } + } + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "SwaggerGenOptions.OperationFilter instantiates this class at runtime")] private class AuthorizeCheckOperationDocumentFilter : IOperationFilter, IDocumentFilter @@ -388,4 +401,9 @@ public static class SwaggerGenOptionsExtensions } } } + + public class SwaggerBodyExampleAttribute(string value) : Attribute + { + public string Value => value; + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Policies/HellthreadRejectPolicy.cs b/Iceshrimp.Backend/Core/Policies/HellthreadRejectPolicy.cs index 3c31ef90..0c7de326 100644 --- a/Iceshrimp.Backend/Core/Policies/HellthreadRejectPolicy.cs +++ b/Iceshrimp.Backend/Core/Policies/HellthreadRejectPolicy.cs @@ -16,7 +16,6 @@ public class HellthreadRejectPolicyConfiguration : IPolicyConfiguration new(Enabled, MentionLimit); IPolicy IPolicyConfiguration. Apply() => Apply(); - public required bool Enabled { get; set; } - - public required int MentionLimit { get; set; } + public bool Enabled { get; set; } + public int MentionLimit { get; set; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Policies/UserAgeRejectPolicy.cs b/Iceshrimp.Backend/Core/Policies/UserAgeRejectPolicy.cs index ee28b1d2..31580826 100644 --- a/Iceshrimp.Backend/Core/Policies/UserAgeRejectPolicy.cs +++ b/Iceshrimp.Backend/Core/Policies/UserAgeRejectPolicy.cs @@ -15,6 +15,6 @@ public class UserAgeRejectPolicyConfiguration : IPolicyConfiguration new(Enabled, Age); IPolicy IPolicyConfiguration.Apply() => Apply(); - public required bool Enabled { get; set; } - public required TimeSpan Age { get; set; } + public bool Enabled { get; set; } + public TimeSpan Age { get; set; } = TimeSpan.Zero; } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Policies/WordRejectPolicy.cs b/Iceshrimp.Backend/Core/Policies/WordRejectPolicy.cs index fee6ef8b..c6d5c371 100644 --- a/Iceshrimp.Backend/Core/Policies/WordRejectPolicy.cs +++ b/Iceshrimp.Backend/Core/Policies/WordRejectPolicy.cs @@ -45,6 +45,6 @@ public class WordRejectPolicyConfiguration : IPolicyConfiguration new(Enabled, Words); IPolicy IPolicyConfiguration.Apply() => Apply(); - public required bool Enabled { get; set; } - public required string[] Words { get; set; } + public bool Enabled { get; set; } + public string[] Words { get; set; } = []; } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/PolicyService.cs b/Iceshrimp.Backend/Core/Services/PolicyService.cs index 07f52cb7..4b2e74c7 100644 --- a/Iceshrimp.Backend/Core/Services/PolicyService.cs +++ b/Iceshrimp.Backend/Core/Services/PolicyService.cs @@ -81,6 +81,39 @@ public class PolicyService(IServiceScopeFactory scopeFactory) foreach (var hook in hooks) hook.Apply(data); } + + public async Task GetConfigurationType(string name) + { + await Initialize(); + var type = _policyTypes.FirstOrDefault(p => p.Name == name); + return _policyConfigurationTypes + .FirstOrDefault(p => p.GetInterfaces() + .FirstOrDefault(i => i.Name == typeof(IPolicyConfiguration<>).Name) + ?.GenericTypeArguments.FirstOrDefault() == + type); + } + + public async Task GetConfiguration(string name, string? data) + { + var type = await GetConfigurationType(name); + if (type == null) return null; + + var cType = _policyConfigurationTypes + .FirstOrDefault(p => p.GetInterfaces() + .FirstOrDefault(i => i.Name == typeof(IPolicyConfiguration<>).Name) + ?.GenericTypeArguments.FirstOrDefault() == + type); + + if (cType == null) return null; + if (data == null) return (IPolicyConfiguration?)Activator.CreateInstance(cType); + return (IPolicyConfiguration?)JsonSerializer.Deserialize(data, cType); + } + + public async Task> GetAvailablePolicies() + { + await Initialize(); + return _policyTypes.Select(p => p.Name).ToList(); + } } public interface IPolicy @@ -115,7 +148,7 @@ public interface IPolicyConfiguration public IPolicy Apply(); } -public interface IPolicyConfiguration : IPolicyConfiguration +public interface IPolicyConfiguration : IPolicyConfiguration where TPolicy : IPolicy { public new TPolicy Apply(); } \ No newline at end of file