From a7b47b59b851b85aecae69e98d0ac2161ebdf949 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 18 Feb 2024 01:39:27 +0100 Subject: [PATCH] [backend/api] Fixup basic endpoints --- .../Controllers/Renderers/TimelineRenderer.cs | 12 ------ .../Controllers/Schemas/PaginationQuery.cs | 10 +++++ .../Controllers/Schemas/TimelineResponse.cs | 9 ----- .../Controllers/UserController.cs | 40 ++++++++++--------- .../Core/Extensions/QueryableExtensions.cs | 40 +++++++++++++++++++ 5 files changed, 72 insertions(+), 39 deletions(-) delete mode 100644 Iceshrimp.Backend/Controllers/Renderers/TimelineRenderer.cs create mode 100644 Iceshrimp.Backend/Controllers/Schemas/PaginationQuery.cs delete mode 100644 Iceshrimp.Backend/Controllers/Schemas/TimelineResponse.cs diff --git a/Iceshrimp.Backend/Controllers/Renderers/TimelineRenderer.cs b/Iceshrimp.Backend/Controllers/Renderers/TimelineRenderer.cs deleted file mode 100644 index 5dfcd31a..00000000 --- a/Iceshrimp.Backend/Controllers/Renderers/TimelineRenderer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Iceshrimp.Backend.Controllers.Schemas; -using Iceshrimp.Backend.Core.Database.Tables; - -namespace Iceshrimp.Backend.Controllers.Renderers; - -public class TimelineRenderer -{ - public static TimelineResponse Render(IEnumerable notes, int limit) - { - return new TimelineResponse { Notes = NoteRenderer.RenderMany(notes), Limit = limit }; - } -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Schemas/PaginationQuery.cs b/Iceshrimp.Backend/Controllers/Schemas/PaginationQuery.cs new file mode 100644 index 00000000..99f33c42 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Schemas/PaginationQuery.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Iceshrimp.Backend.Controllers.Schemas; + +public class PaginationQuery +{ + [FromQuery(Name = "max_id")] public string? MaxId { get; set; } + [FromQuery(Name = "min_id")] public string? MinId { get; set; } + [FromQuery(Name = "limit")] public int? Limit { get; set; } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Schemas/TimelineResponse.cs b/Iceshrimp.Backend/Controllers/Schemas/TimelineResponse.cs deleted file mode 100644 index 0b642b84..00000000 --- a/Iceshrimp.Backend/Controllers/Schemas/TimelineResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; - -namespace Iceshrimp.Backend.Controllers.Schemas; - -public class TimelineResponse -{ - [J("notes")] public required IEnumerable Notes { get; set; } - [J("limit")] public required int Limit { get; set; } -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/UserController.cs b/Iceshrimp.Backend/Controllers/UserController.cs index 087aa7d2..6a389b07 100644 --- a/Iceshrimp.Backend/Controllers/UserController.cs +++ b/Iceshrimp.Backend/Controllers/UserController.cs @@ -1,8 +1,9 @@ +using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Renderers; using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Core.Database; -using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; @@ -10,7 +11,6 @@ using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers; [ApiController] -[Tags("User")] [Produces("application/json")] [EnableRateLimiting("sliding")] [Route("/api/iceshrimp/v1/user/{id}")] @@ -21,28 +21,32 @@ public class UserController(DatabaseContext db) : Controller [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] public async Task GetUser(string id) { - var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id); - if (user == null) return NotFound(); - return Ok(user); + var user = await db.Users.IncludeCommonProperties() + .FirstOrDefaultAsync(p => p.Id == id) ?? + throw GracefulException.NotFound("User not found"); + + return Ok(UserRenderer.RenderOne(user)); } [HttpGet("notes")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))] + [Authenticate] + [LinkPagination(20, 80)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] - public async Task GetUserNotes(string id) + public async Task GetUserNotes(string id, PaginationQuery pq) { - var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id); - if (user == null) return NotFound(); + var localUser = HttpContext.GetUser(); + var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id) ?? + throw GracefulException.NotFound("User not found"); - var limit = 10; - var notes = db.Notes - .Include(p => p.User) - .Where(p => p.UserId == id) - .HasVisibility(Note.NoteVisibility.Public) - .OrderByDescending(p => p.Id) - .Take(limit) - .ToList(); + var notes = await db.Notes + .IncludeCommonProperties() + .Where(p => p.User == user) + .EnsureVisibleFor(localUser) + .PrecomputeVisibilities(localUser) + .Paginate(pq, ControllerContext) + .ToListAsync(); - return Ok(TimelineRenderer.Render(notes, limit)); + return Ok(NoteRenderer.RenderMany(notes.EnforceRenoteReplyVisibility())); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index e32acd77..8e59c3c2 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -4,6 +4,7 @@ using Iceshrimp.Backend.Controllers.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Middleware; @@ -48,6 +49,45 @@ public static class QueryableExtensions return query.Take(Math.Min(pq.Limit ?? defaultLimit, maxLimit)); } + public static IQueryable Paginate( + this IQueryable query, + PaginationQuery pq, + int defaultLimit, + int maxLimit + ) where T : IEntity + { + if (pq.Limit is < 1) + throw GracefulException.BadRequest("Limit cannot be less than 1"); + + query = pq switch + { + { MinId: not null, MaxId: not null } => query + .Where(p => p.Id.IsGreaterThan(pq.MinId) && + p.Id.IsLessThan(pq.MaxId)) + .OrderBy(p => p.Id), + { MinId: not null } => query.Where(p => p.Id.IsGreaterThan(pq.MinId)).OrderBy(p => p.Id), + { MaxId: not null } => query.Where(p => p.Id.IsLessThan(pq.MaxId)).OrderByDescending(p => p.Id), + _ => query.OrderByDescending(p => p.Id) + }; + + return query.Take(Math.Min(pq.Limit ?? defaultLimit, maxLimit)); + } + + public static IQueryable Paginate( + this IQueryable query, + MastodonPaginationQuery pq, + ControllerContext context + ) where T : IEntity + { + var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) + .OfType() + .FirstOrDefault(); + if (filter == null) + throw new GracefulException("Route doesn't have a LinkPaginationAttribute"); + + return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); + } + public static IQueryable Paginate( this IQueryable query, PaginationQuery pq,