[backend/api] Add filter endpoints (ISH-339)

This commit is contained in:
Laura Hausmann 2024-07-13 00:36:49 +02:00
parent e4b0f32097
commit f42aeee2fd
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
10 changed files with 230 additions and 9 deletions

View file

@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
@ -8,7 +9,11 @@ public class FilterSchemas
{
public class CreateFilterRequest
{
[B(Name = "title")] [J("title")] [JR] public required string Title { get; set; }
[MinLength(1)]
[B(Name = "title")]
[J("title")]
[JR]
public required string Title { get; set; }
[B(Name = "context")]
[J("context")]

View file

@ -0,0 +1,85 @@
using System.Net;
using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Controllers.Web.Renderers;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Web;
[ApiController]
[Authenticate]
[Authorize]
[EnableRateLimiting("sliding")]
[Route("/api/iceshrimp/filters")]
[Produces(MediaTypeNames.Application.Json)]
public class FilterController(DatabaseContext db, EventService eventSvc) : ControllerBase
{
[HttpGet]
[ProducesResults(HttpStatusCode.OK)]
public async Task<IEnumerable<FilterResponse>> GetFilters()
{
var user = HttpContext.GetUserOrFail();
var filters = await db.Filters.Where(p => p.User == user).ToListAsync();
return FilterRenderer.RenderMany(filters);
}
[HttpPost]
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.BadRequest)]
public async Task<FilterResponse> CreateFilter(FilterRequest request)
{
var user = HttpContext.GetUserOrFail();
var filter = new Filter
{
User = user,
Name = request.Name,
Expiry = request.Expiry,
Keywords = request.Keywords,
Action = (Filter.FilterAction)request.Action,
Contexts = request.Contexts.Cast<Filter.FilterContext>().ToList()
};
db.Add(filter);
await db.SaveChangesAsync();
eventSvc.RaiseFilterAdded(this, filter);
return FilterRenderer.RenderOne(filter);
}
[HttpPut("{id:long}")]
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
public async Task UpdateFilter(long id, FilterRequest request)
{
var user = HttpContext.GetUserOrFail();
var filter = await db.Filters.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
throw GracefulException.NotFound("Filter not found");
filter.Name = request.Name;
filter.Expiry = request.Expiry;
filter.Keywords = request.Keywords;
filter.Action = (Filter.FilterAction)request.Action;
filter.Contexts = request.Contexts.Cast<Filter.FilterContext>().ToList();
await db.SaveChangesAsync();
eventSvc.RaiseFilterUpdated(this, filter);
}
[HttpDelete("{id:long}")]
[ProducesResults(HttpStatusCode.OK, HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
public async Task DeleteFilter(long id)
{
var user = HttpContext.GetUserOrFail();
var filter = await db.Filters.FirstOrDefaultAsync(p => p.User == user && p.Id == id) ??
throw GracefulException.NotFound("Filter not found");
db.Remove(filter);
await db.SaveChangesAsync();
eventSvc.RaiseFilterRemoved(this, filter);
}
}

View file

@ -0,0 +1,19 @@
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Shared.Schemas.Web;
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
public static class FilterRenderer
{
public static FilterResponse RenderOne(Filter filter) => new()
{
Id = filter.Id,
Name = filter.Name,
Expiry = filter.Expiry,
Keywords = filter.Keywords,
Action = (FilterResponse.FilterAction)filter.Action,
Contexts = filter.Contexts.Cast<FilterResponse.FilterContext>().ToList(),
};
public static IEnumerable<FilterResponse> RenderMany(IEnumerable<Filter> filters) => filters.Select(RenderOne);
}

View file

@ -13,19 +13,19 @@ public class Filter
[PgName("filter_action_enum")]
public enum FilterAction
{
[PgName("warn")] Warn,
[PgName("hide")] Hide
[PgName("warn")] Warn = 0,
[PgName("hide")] Hide = 1
}
[PgName("filter_context_enum")]
public enum FilterContext
{
[PgName("home")] Home,
[PgName("lists")] Lists,
[PgName("threads")] Threads,
[PgName("notifications")] Notifications,
[PgName("accounts")] Accounts,
[PgName("public")] Public
[PgName("home")] Home = 0,
[PgName("lists")] Lists = 1,
[PgName("threads")] Threads = 2,
[PgName("notifications")] Notifications = 3,
[PgName("accounts")] Accounts = 4,
[PgName("public")] Public = 5
}
[Key]

View file

@ -54,6 +54,9 @@ public sealed class StreamingConnectionAggregate : IDisposable
_eventService.UserUnmuted -= OnUserUnmute;
_eventService.UserFollowed -= OnUserFollow;
_eventService.UserUnfollowed -= OnUserUnfollow;
_eventService.FilterAdded -= OnFilterAdded;
_eventService.FilterUpdated -= OnFilterUpdated;
_eventService.FilterRemoved -= OnFilterRemoved;
_scope.Dispose();
}
@ -234,6 +237,10 @@ public sealed class StreamingConnectionAggregate : IDisposable
_eventService.Notification += OnNotification;
_streamingService.NotePublished += OnNotePublished;
_streamingService.NoteUpdated += OnNoteUpdated;
_eventService.FilterAdded += OnFilterAdded;
_eventService.FilterUpdated += OnFilterUpdated;
_eventService.FilterRemoved += OnFilterRemoved;
}
private async Task InitializeRelationships()
@ -372,6 +379,28 @@ public sealed class StreamingConnectionAggregate : IDisposable
#endregion
#region Filter event handlers
private async void OnFilterAdded(object? _, Filter filter)
{
if (filter.User.Id != _userId) return;
await _hub.Clients.User(_userId).FilterAdded(FilterRenderer.RenderOne(filter));
}
private async void OnFilterUpdated(object? _, Filter filter)
{
if (filter.User.Id != _userId) return;
await _hub.Clients.User(_userId).FilterUpdated(FilterRenderer.RenderOne(filter));
}
private async void OnFilterRemoved(object? _, Filter filter)
{
if (filter.User.Id != _userId) return;
await _hub.Clients.User(_userId).FilterRemoved(filter.Id);
}
#endregion
#region Connection status methods
public void Connect(string connectionId)

View file

@ -0,0 +1,18 @@
using Iceshrimp.Frontend.Core.Services;
using Iceshrimp.Shared.Schemas.Web;
namespace Iceshrimp.Frontend.Core.ControllerModels;
internal class FilterControllerModel(ApiClient api)
{
public Task<IEnumerable<FilterResponse>> GetFilters() =>
api.Call<IEnumerable<FilterResponse>>(HttpMethod.Get, "/filters");
public Task<FilterResponse> CreateFilter(FilterRequest request) =>
api.Call<FilterResponse>(HttpMethod.Post, "/filters", data: request);
public Task UpdateFilter(long id, FilterRequest request) =>
api.Call(HttpMethod.Put, $"/filters/{id}", data: request);
public Task DeleteFilter(long id) => api.Call(HttpMethod.Delete, $"/filters/{id}");
}

View file

@ -32,6 +32,9 @@ internal class StreamingService(
public event EventHandler<NotificationResponse>? Notification;
public event EventHandler<NoteEvent>? NotePublished;
public event EventHandler<NoteEvent>? NoteUpdated;
public event EventHandler<FilterResponse>? FilterAdded;
public event EventHandler<FilterResponse>? FilterUpdated;
public event EventHandler<long>? FilterRemoved;
public event EventHandler<HubConnectionState>? OnConnectionChange;
public async Task Connect(StoredUser? user = null)
@ -125,5 +128,23 @@ internal class StreamingService(
streaming.NoteUpdated?.Invoke(this, (timeline, note));
return Task.CompletedTask;
}
public Task FilterAdded(FilterResponse filter)
{
streaming.FilterAdded?.Invoke(this, filter);
return Task.CompletedTask;
}
public Task FilterUpdated(FilterResponse filter)
{
streaming.FilterUpdated?.Invoke(this, filter);
return Task.CompletedTask;
}
public Task FilterRemoved(long filterId)
{
streaming.FilterRemoved?.Invoke(this, filterId);
return Task.CompletedTask;
}
}
}

View file

@ -13,6 +13,10 @@ public interface IStreamingHubClient
public Task Notification(NotificationResponse notification);
public Task NotePublished(List<StreamingTimeline> timelines, NoteResponse note);
public Task NoteUpdated(List<StreamingTimeline> timelines, NoteResponse note);
public Task FilterAdded(FilterResponse filter);
public Task FilterUpdated(FilterResponse filter);
public Task FilterRemoved(long filterId);
}
public enum StreamingTimeline

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Iceshrimp.Shared.Schemas.Web;
public class FilterRequest
{
[MinLength(1)] public required string Name { get; init; }
public required DateTime? Expiry { get; init; }
public required List<string> Keywords { get; init; }
public required FilterResponse.FilterAction Action { get; init; }
public required List<FilterResponse.FilterContext> Contexts { get; init; }
}

View file

@ -0,0 +1,27 @@
namespace Iceshrimp.Shared.Schemas.Web;
public class FilterResponse
{
public required long Id { get; init; }
public required string Name { get; init; }
public required DateTime? Expiry { get; init; }
public required List<string> Keywords { get; init; }
public required FilterAction Action { get; init; }
public required List<FilterContext> Contexts { get; init; }
public enum FilterContext
{
Home = 0,
Lists = 1,
Threads = 2,
Notifications = 3,
Accounts = 4,
Public = 5
}
public enum FilterAction
{
Warn = 0,
Hide = 1
}
}