diff --git a/Iceshrimp.Backend/Controllers/Attributes/LinkPaginationAttribute.cs b/Iceshrimp.Backend/Controllers/Attributes/LinkPaginationAttribute.cs index 3c189e75..c382d921 100644 --- a/Iceshrimp.Backend/Controllers/Attributes/LinkPaginationAttribute.cs +++ b/Iceshrimp.Backend/Controllers/Attributes/LinkPaginationAttribute.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Core.Database; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -5,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace Iceshrimp.Backend.Controllers.Attributes; -public class LinkPaginationAttribute(int defaultLimit, int maxLimit) : ActionFilterAttribute +public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset = false) : ActionFilterAttribute { public int DefaultLimit => defaultLimit; public int MaxLimit => maxLimit; @@ -39,14 +40,23 @@ public class LinkPaginationAttribute(int defaultLimit, int maxLimit) : ActionFil var limit = Math.Min(query.Limit ?? defaultLimit, maxLimit); var request = context.HttpContext.Request; + var mpq = query as MastodonPaginationQuery; + var offsetPg = offset || mpq is { Offset: not null, MaxId: null, MinId: null, SinceId: null }; if (ids.Count >= limit) { - var next = new QueryBuilder { { "limit", limit.ToString() }, { "max_id", ids.Last() } }; + var next = offsetPg + ? new QueryBuilder { { "offset", ((mpq?.Offset ?? 0) + limit).ToString() } } + : new QueryBuilder { { "limit", limit.ToString() }, { "max_id", ids.Last() } }; + links.Add($"<{GetUrl(request, next.ToQueryString())}>; rel=\"next\""); } - var prev = new QueryBuilder { { "limit", limit.ToString() }, { "min_id", ids.First() } }; - links.Add($"<{GetUrl(request, prev.ToQueryString())}>; rel=\"prev\""); + var prev = offsetPg + ? new QueryBuilder { { "offset", Math.Max(0, (mpq?.Offset ?? 0) - limit).ToString() } } + : new QueryBuilder { { "limit", limit.ToString() }, { "min_id", ids.First() } }; + + if (!offsetPg || (mpq?.Offset ?? 0) != 0) + links.Add($"<{GetUrl(request, prev.ToQueryString())}>; rel=\"prev\""); context.HttpContext.Response.Headers.Link = string.Join(", ", links); } diff --git a/Iceshrimp.Backend/Controllers/Mastodon/SearchController.cs b/Iceshrimp.Backend/Controllers/Mastodon/SearchController.cs index 338ef311..d88eff90 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/SearchController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/SearchController.cs @@ -56,7 +56,7 @@ public class SearchController( [HttpGet("/api/v1/accounts/search")] [Authorize("read:accounts")] - [LinkPagination(20, 40)] + [LinkPagination(20, 40, true)] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] public async Task SearchAccounts( @@ -133,8 +133,8 @@ public class SearchController( .Where(p => p.DisplayNameOrUsernameOrFqnContainsCaseInsensitive(search.Query!, config.Value.AccountDomain)) .Where(p => !search.Following || p.IsFollowedBy(user)) - .Paginate(pagination, ControllerContext) //TODO: this will mess up our sorting .OrderByDescending(p => p.NotesCount) + .PaginateByOffset(pagination, ControllerContext) .RenderAllForMastodonAsync(userRenderer); } diff --git a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs index e9823e9e..f6564ab6 100644 --- a/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/QueryableExtensions.cs @@ -178,6 +178,34 @@ public static class QueryableExtensions return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); } + + public static IQueryable PaginateByOffset( + this IQueryable query, + MastodonPaginationQuery pq, + int defaultLimit, + int maxLimit + ) where T : IEntity + { + if (pq.Limit is < 1) + throw GracefulException.BadRequest("Limit cannot be less than 1"); + + return query.Skip(pq.Offset ?? 0).Take(Math.Min(pq.Limit ?? defaultLimit, maxLimit)); + } + + public static IQueryable PaginateByOffset( + 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 PaginateByOffset(query, pq, filter.DefaultLimit, filter.MaxLimit); + } public static IQueryable Paginate( this IQueryable query,