diff --git a/Iceshrimp.Backend/Controllers/Shared/Attributes/LinkPaginationAttribute.cs b/Iceshrimp.Backend/Controllers/Shared/Attributes/LinkPaginationAttribute.cs index 2c95f672..e491189c 100644 --- a/Iceshrimp.Backend/Controllers/Shared/Attributes/LinkPaginationAttribute.cs +++ b/Iceshrimp.Backend/Controllers/Shared/Attributes/LinkPaginationAttribute.cs @@ -7,7 +7,11 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Iceshrimp.Backend.Controllers.Shared.Attributes; -public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset = false) : ActionFilterAttribute +public class LinkPaginationAttribute( + int defaultLimit, + int maxLimit, + bool offset = false +) : ActionFilterAttribute, IPaginationAttribute { public int DefaultLimit => defaultLimit; public int MaxLimit => maxLimit; @@ -75,6 +79,12 @@ public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset } } +public interface IPaginationAttribute +{ + public int DefaultLimit { get; } + public int MaxLimit { get; } +} + public static class HttpContextExtensions { private const string Key = "link-pagination"; diff --git a/Iceshrimp.Backend/Controllers/Web/NoteController.cs b/Iceshrimp.Backend/Controllers/Web/NoteController.cs index 0ee309ff..6892f8e1 100644 --- a/Iceshrimp.Backend/Controllers/Web/NoteController.cs +++ b/Iceshrimp.Backend/Controllers/Web/NoteController.cs @@ -13,6 +13,7 @@ using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; using Iceshrimp.Shared.Schemas.Web; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; @@ -217,10 +218,10 @@ public class NoteController( [HttpGet("{id}/likes")] [Authenticate] [Authorize] - [LinkPagination(20, 40)] + [RestPagination(20, 40)] [ProducesResults(HttpStatusCode.OK)] [ProducesErrors(HttpStatusCode.NotFound)] - public async Task> GetNoteLikes(string id, PaginationQuery pq) + public async Task>> GetNoteLikes(string id, PaginationQuery pq) { var user = HttpContext.GetUser(); var note = await db.Notes @@ -236,8 +237,8 @@ public class NoteController( .Wrap(p => p.User) .ToListAsync(); - HttpContext.SetPaginationData(users); - return await userRenderer.RenderMany(users.Select(p => p.Entity)); + var res = await userRenderer.RenderMany(users.Select(p => p.Entity)); + return HttpContext.CreatePaginationWrapper(pq, users, res); } [HttpPost("{id}/renote")] diff --git a/Iceshrimp.Backend/Core/Extensions/HttpContextExtensions.cs b/Iceshrimp.Backend/Core/Extensions/HttpContextExtensions.cs new file mode 100644 index 00000000..6cdc2432 --- /dev/null +++ b/Iceshrimp.Backend/Core/Extensions/HttpContextExtensions.cs @@ -0,0 +1,50 @@ +using Iceshrimp.Backend.Controllers.Shared.Attributes; +using Iceshrimp.Backend.Controllers.Shared.Schemas; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Middleware; +using Iceshrimp.Shared.Schemas.Web; +using Microsoft.AspNetCore.Http.Extensions; + +namespace Iceshrimp.Backend.Core.Extensions; + +public static partial class HttpContextExtensions +{ + public static PaginationWrapper CreatePaginationWrapper( + this HttpContext ctx, PaginationQuery query, IEnumerable paginationData, TData data + ) + { + var attr = ctx.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) throw new Exception("Route doesn't have a RestPaginationAttribute"); + + var limit = Math.Min(query.Limit ?? attr.DefaultLimit, attr.MaxLimit); + if (limit < 1) throw GracefulException.BadRequest("Limit cannot be less than 1"); + + var ids = paginationData.Select(p => p.Id).ToList(); + if (query.MinId != null) ids.Reverse(); + + var next = ids.Count >= limit ? new QueryBuilder { { "max_id", ids.Last() } } : null; + var prev = ids.Count > 0 ? new QueryBuilder { { "min_id", ids.First() } } : null; + + var links = new PaginationData + { + Limit = limit, + Next = next?.ToQueryString().ToString(), + Prev = prev?.ToQueryString().ToString() + }; + + return new PaginationWrapper { Data = data, Links = links }; + } + + public static PaginationWrapper CreatePaginationWrapper( + this HttpContext ctx, PaginationQuery query, TData data + ) where TData : IEnumerable + { + return CreatePaginationWrapper(ctx, query, data, data); + } +} + +public class RestPaginationAttribute(int defaultLimit, int maxLimit) : Attribute, IPaginationAttribute +{ + public int DefaultLimit => defaultLimit; + public int MaxLimit => maxLimit; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index 2cce4540..f1801839 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -276,13 +276,11 @@ public static class QueryableExtensions ControllerContext context ) where T : IEntity { - var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) - .OfType() - .FirstOrDefault(); - if (filter == null) - throw new Exception("Route doesn't have a LinkPaginationAttribute"); + var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) + throw new Exception("Route doesn't have a IPaginationAttribute"); - return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); + return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit); } public static IQueryable PaginateByOffset( @@ -304,13 +302,11 @@ public static class QueryableExtensions ControllerContext context ) where T : IEntity { - var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) - .OfType() - .FirstOrDefault(); - if (filter == null) - throw new Exception("Route doesn't have a LinkPaginationAttribute"); + var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) + throw new Exception("Route doesn't have a IPaginationAttribute"); - return PaginateByOffset(query, pq, filter.DefaultLimit, filter.MaxLimit); + return PaginateByOffset(query, pq, attr.DefaultLimit, attr.MaxLimit); } public static IQueryable Paginate( @@ -320,13 +316,11 @@ public static class QueryableExtensions ControllerContext context ) where T : IEntity { - var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) - .OfType() - .FirstOrDefault(); - if (filter == null) - throw new Exception("Route doesn't have a LinkPaginationAttribute"); + var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) + throw new Exception("Route doesn't have a IPaginationAttribute"); - return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit); + return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit); } public static IQueryable Paginate( @@ -336,13 +330,11 @@ public static class QueryableExtensions ControllerContext context ) where T : IEntity { - var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) - .OfType() - .FirstOrDefault(); - if (filter == null) - throw new Exception("Route doesn't have a LinkPaginationAttribute"); + var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) + throw new Exception("Route doesn't have a IPaginationAttribute"); - return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit); + return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit); } public static IQueryable Paginate( @@ -351,13 +343,11 @@ public static class QueryableExtensions ControllerContext context ) where T : IEntity { - var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) - .OfType() - .FirstOrDefault(); - if (filter == null) - throw new Exception("Route doesn't have a LinkPaginationAttribute"); + var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (attr == null) + throw new Exception("Route doesn't have a IPaginationAttribute"); - return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); + return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit); } public static IQueryable> Wrap( diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index ed4d1071..926d7bc0 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -365,7 +365,7 @@ public static class ServiceExtensions } } -public static class HttpContextExtensions +public static partial class HttpContextExtensions { public static string GetRateLimitPartition(this HttpContext ctx, bool includeRoute) => (includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? ""); diff --git a/Iceshrimp.Shared/Schemas/Web/PaginationWrapper.cs b/Iceshrimp.Shared/Schemas/Web/PaginationWrapper.cs new file mode 100644 index 00000000..b562a8de --- /dev/null +++ b/Iceshrimp.Shared/Schemas/Web/PaginationWrapper.cs @@ -0,0 +1,14 @@ +namespace Iceshrimp.Shared.Schemas.Web; + +public class PaginationWrapper +{ + public required PaginationData Links { get; set; } + public required TData Data { get; set; } +} + +public class PaginationData +{ + public required int Limit { get; set; } + public string? Next { get; set; } + public string? Prev { get; set; } +} \ No newline at end of file