[backend/core] Allow configuring arbitrary reject/rewrite policies, add default configuration values to all policies (ISH-16)

This commit is contained in:
Laura Hausmann 2024-10-09 05:46:09 +02:00
parent a5a2c0b169
commit dc77c48005
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
6 changed files with 79 additions and 82 deletions

View file

@ -12,7 +12,6 @@ using Iceshrimp.Backend.Core.Federation.ActivityStreams;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Policies;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Backend.Core.Tasks; using Iceshrimp.Backend.Core.Tasks;
using Iceshrimp.Shared.Schemas.Web; using Iceshrimp.Shared.Schemas.Web;
@ -20,6 +19,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static Iceshrimp.Backend.Core.Extensions.SwaggerGenOptionsExtensions;
namespace Iceshrimp.Backend.Controllers.Web; namespace Iceshrimp.Backend.Controllers.Web;
@ -187,87 +187,34 @@ public class AdminController(
await new MediaCleanupTask().Invoke(scope.ServiceProvider); await new MediaCleanupTask().Invoke(scope.ServiceProvider);
} }
[HttpGet("policy/word-reject")] [HttpGet("policy")]
[ProducesResults(HttpStatusCode.OK)] [ProducesResults(HttpStatusCode.OK)]
public async Task<WordRejectPolicyConfiguration> GetWordRejectPolicy() public async Task<List<string>> GetAvailablePolicies() => await policySvc.GetAvailablePolicies();
{
var raw = await db.PolicyConfiguration.Where(p => p.Name == nameof(WordRejectPolicy))
.Select(p => p.Data)
.FirstOrDefaultAsync();
var res = raw != null ? JsonSerializer.Deserialize<WordRejectPolicyConfiguration>(raw) : null; [HttpGet("policy/{name}")]
res ??= new WordRejectPolicyConfiguration { Enabled = false, Words = [] }; [ProducesResults(HttpStatusCode.OK)]
return res; [ProducesErrors(HttpStatusCode.NotFound)]
public async Task<IPolicyConfiguration> 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)] [ProducesResults(HttpStatusCode.OK)]
public async Task<UserAgeRejectPolicyConfiguration> 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)) // @formatter:off
.Select(p => p.Data) var type = await policySvc.GetConfigurationType(name) ?? throw GracefulException.NotFound("Policy not found");
.FirstOrDefaultAsync(); 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<UserAgeRejectPolicyConfiguration>(raw) : null;
res ??= new UserAgeRejectPolicyConfiguration { Enabled = false, Age = TimeSpan.Zero };
return res;
}
[HttpGet("policy/hellthread-reject")]
[ProducesResults(HttpStatusCode.OK)]
public async Task<HellthreadRejectPolicyConfiguration> GetHellthreadRejectPolicy()
{
var raw = await db.PolicyConfiguration.Where(p => p.Name == nameof(HellthreadRejectPolicy))
.Select(p => p.Data)
.FirstOrDefaultAsync();
var res = raw != null ? JsonSerializer.Deserialize<HellthreadRejectPolicyConfiguration>(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 await db.PolicyConfiguration
.Upsert(new PolicyConfiguration .Upsert(new PolicyConfiguration { Name = name, Data = JsonSerializer.Serialize(data) })
{
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)
})
.On(p => new { p.Name }) .On(p => new { p.Name })
.RunAsync(); .RunAsync();

View file

@ -19,6 +19,7 @@ public static class SwaggerGenOptionsExtensions
public static void AddFilters(this SwaggerGenOptions options) public static void AddFilters(this SwaggerGenOptions options)
{ {
options.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>(); options.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
options.SchemaFilter<SwaggerBodyExampleSchemaFilter>();
options.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately. options.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately.
options.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable options.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable
options.UseAllOfForInheritance(); // Allows $ref objects 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<T> instantiates this class at runtime")]
private class SwaggerBodyExampleSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var att = context.ParameterInfo?.GetCustomAttribute<SwaggerBodyExampleAttribute>();
if (att != null)
schema.Example = new OpenApiString(att.Value);
}
}
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local",
Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")] Justification = "SwaggerGenOptions.OperationFilter<T> instantiates this class at runtime")]
private class AuthorizeCheckOperationDocumentFilter : IOperationFilter, IDocumentFilter private class AuthorizeCheckOperationDocumentFilter : IOperationFilter, IDocumentFilter
@ -388,4 +401,9 @@ public static class SwaggerGenOptionsExtensions
} }
} }
} }
public class SwaggerBodyExampleAttribute(string value) : Attribute
{
public string Value => value;
}
} }

View file

@ -16,7 +16,6 @@ public class HellthreadRejectPolicyConfiguration : IPolicyConfiguration<Hellthre
public HellthreadRejectPolicy Apply() => new(Enabled, MentionLimit); public HellthreadRejectPolicy Apply() => new(Enabled, MentionLimit);
IPolicy IPolicyConfiguration. Apply() => Apply(); IPolicy IPolicyConfiguration. Apply() => Apply();
public required bool Enabled { get; set; } public bool Enabled { get; set; }
public int MentionLimit { get; set; }
public required int MentionLimit { get; set; }
} }

View file

@ -15,6 +15,6 @@ public class UserAgeRejectPolicyConfiguration : IPolicyConfiguration<UserAgeReje
public UserAgeRejectPolicy Apply() => new(Enabled, Age); public UserAgeRejectPolicy Apply() => new(Enabled, Age);
IPolicy IPolicyConfiguration.Apply() => Apply(); IPolicy IPolicyConfiguration.Apply() => Apply();
public required bool Enabled { get; set; } public bool Enabled { get; set; }
public required TimeSpan Age { get; set; } public TimeSpan Age { get; set; } = TimeSpan.Zero;
} }

View file

@ -45,6 +45,6 @@ public class WordRejectPolicyConfiguration : IPolicyConfiguration<WordRejectPoli
public WordRejectPolicy Apply() => new(Enabled, Words); public WordRejectPolicy Apply() => new(Enabled, Words);
IPolicy IPolicyConfiguration.Apply() => Apply(); IPolicy IPolicyConfiguration.Apply() => Apply();
public required bool Enabled { get; set; } public bool Enabled { get; set; }
public required string[] Words { get; set; } public string[] Words { get; set; } = [];
} }

View file

@ -81,6 +81,39 @@ public class PolicyService(IServiceScopeFactory scopeFactory)
foreach (var hook in hooks) hook.Apply(data); foreach (var hook in hooks) hook.Apply(data);
} }
public async Task<Type?> 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<IPolicyConfiguration?> 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<List<string>> GetAvailablePolicies()
{
await Initialize();
return _policyTypes.Select(p => p.Name).ToList();
}
} }
public interface IPolicy public interface IPolicy
@ -115,7 +148,7 @@ public interface IPolicyConfiguration
public IPolicy Apply(); public IPolicy Apply();
} }
public interface IPolicyConfiguration<out TPolicy> : IPolicyConfiguration public interface IPolicyConfiguration<out TPolicy> : IPolicyConfiguration where TPolicy : IPolicy
{ {
public new TPolicy Apply(); public new TPolicy Apply();
} }