[backend/api] Add filter endpoints (ISH-339)
This commit is contained in:
parent
e4b0f32097
commit
f42aeee2fd
10 changed files with 230 additions and 9 deletions
|
@ -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")]
|
||||
|
|
85
Iceshrimp.Backend/Controllers/Web/FilterController.cs
Normal file
85
Iceshrimp.Backend/Controllers/Web/FilterController.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}");
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
13
Iceshrimp.Shared/Schemas/Web/FilterRequest.cs
Normal file
13
Iceshrimp.Shared/Schemas/Web/FilterRequest.cs
Normal 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; }
|
||||
}
|
27
Iceshrimp.Shared/Schemas/Web/FilterResponse.cs
Normal file
27
Iceshrimp.Shared/Schemas/Web/FilterResponse.cs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue