From 081b836e9210d1fb9afd78df17e328fca412d1e7 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 6 Oct 2023 21:59:24 +0200 Subject: [PATCH] [mastodon-client] Move link header pagination to middleware --- .../server/api/mastodon/endpoints/account.ts | 14 +++--- .../src/server/api/mastodon/endpoints/list.ts | 2 +- .../server/api/mastodon/endpoints/status.ts | 4 +- .../server/api/mastodon/endpoints/timeline.ts | 2 +- .../src/server/api/mastodon/helpers/list.ts | 9 ++-- .../src/server/api/mastodon/helpers/note.ts | 10 ++-- .../server/api/mastodon/helpers/pagination.ts | 16 ------ .../server/api/mastodon/helpers/timeline.ts | 10 ++-- .../src/server/api/mastodon/helpers/user.ts | 49 ++++++++++++------- .../backend/src/server/api/mastodon/index.ts | 4 +- .../api/mastodon/middleware/pagination.ts | 33 +++++++++++++ 11 files changed, 98 insertions(+), 55 deletions(-) create mode 100644 packages/backend/src/server/api/mastodon/middleware/pagination.ts diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 0a2fa9d84..4fe34c0af 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -81,7 +81,7 @@ export function setupEndpointsAccount(router: Router): void { const followers = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = followers.map((account) => convertAccountId(account)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; }, ); router.get<{ Params: { id: string } }>( @@ -95,7 +95,7 @@ export function setupEndpointsAccount(router: Router): void { const following = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = following.map((account) => convertAccountId(account)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; }, ); router.get<{ Params: { id: string } }>( @@ -181,7 +181,7 @@ export function setupEndpointsAccount(router: Router): void { const res = await UserHelpers.getUserBookmarks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const bookmarks = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = bookmarks.map(s => convertStatusIds(s)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); + ctx.pagination = res.pagination; } ); router.get("/v1/favourites", @@ -191,7 +191,7 @@ export function setupEndpointsAccount(router: Router): void { const res = await UserHelpers.getUserFavorites(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const favorites = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache); ctx.body = favorites.map(s => convertStatusIds(s)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); + ctx.pagination = res.pagination; } ); router.get("/v1/mutes", @@ -200,7 +200,7 @@ export function setupEndpointsAccount(router: Router): void { const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const res = await UserHelpers.getUserMutes(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, ctx.cache); ctx.body = res.data.map(m => convertAccountId(m)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; } ); router.get("/v1/blocks", @@ -210,7 +210,7 @@ export function setupEndpointsAccount(router: Router): void { const res = await UserHelpers.getUserBlocks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const blocks = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = blocks.map(b => convertAccountId(b)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; } ); router.get("/v1/follow_requests", @@ -220,7 +220,7 @@ export function setupEndpointsAccount(router: Router): void { const res = await UserHelpers.getUserFollowRequests(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const requests = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = requests.map(b => convertAccountId(b)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; } ); router.post<{ Params: { id: string } }>( diff --git a/packages/backend/src/server/api/mastodon/endpoints/list.ts b/packages/backend/src/server/api/mastodon/endpoints/list.ts index b08ce9ade..f3b40562d 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/list.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/list.ts @@ -75,7 +75,7 @@ export function setupEndpointsList(router: Router): void { const accounts = await UserConverter.encodeMany(res.data); ctx.body = accounts.map(account => convertAccountId(account)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; }, ); router.post<{ Params: { id: string } }>( diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 18a93bf05..f63b14d60 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -118,7 +118,7 @@ export function setupEndpointsStatus(router: Router): void { const res = await NoteHelpers.getNoteRebloggedBy(note, ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const users = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = users.map(m => convertAccountId(m)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; } ); router.get<{ Params: { id: string } }>( @@ -131,7 +131,7 @@ export function setupEndpointsStatus(router: Router): void { const res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit); const users = await UserConverter.encodeMany(res.data, ctx.cache); ctx.body = users.map(m => convertAccountId(m)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); + ctx.pagination = res.pagination; } ); router.post<{ Params: { id: string } }>( diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index e7fe23597..1cdf2cc6e 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -121,7 +121,7 @@ export function setupEndpointsTimeline(router: Router): void { const res = await TimelineHelpers.getConversations(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); ctx.body = res.data.map(c => convertConversationIds(c)); - PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); + ctx.pagination = res.pagination; } ); } diff --git a/packages/backend/src/server/api/mastodon/helpers/list.ts b/packages/backend/src/server/api/mastodon/helpers/list.ts index 191e25244..30d17f805 100644 --- a/packages/backend/src/server/api/mastodon/helpers/list.ts +++ b/packages/backend/src/server/api/mastodon/helpers/list.ts @@ -1,6 +1,6 @@ import { ILocalUser, User } from "@/models/entities/user.js"; import { Blockings, UserListJoinings, UserLists, Users } from "@/models/index.js"; -import { LinkPaginationObject } from "@/server/api/mastodon/helpers/user.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { UserList } from "@/models/entities/user-list.js"; import { pushUserToUserList } from "@/services/user-list/push.js"; @@ -54,8 +54,11 @@ export class ListHelpers { return { data: users, - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } diff --git a/packages/backend/src/server/api/mastodon/helpers/note.ts b/packages/backend/src/server/api/mastodon/helpers/note.ts index 6476223a4..72d51b24e 100644 --- a/packages/backend/src/server/api/mastodon/helpers/note.ts +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -14,7 +14,8 @@ import deleteNote from "@/services/note/delete.js"; import { genId } from "@/misc/gen-id.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js"; -import { LinkPaginationObject, UserHelpers } from "@/server/api/mastodon/helpers/user.js"; +import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js" import { addPinned, removePinned } from "@/services/i/pin.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { convertId, IdType } from "@/misc/convert-id.js"; @@ -158,8 +159,11 @@ export class NoteHelpers { return { data: users, - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } diff --git a/packages/backend/src/server/api/mastodon/helpers/pagination.ts b/packages/backend/src/server/api/mastodon/helpers/pagination.ts index ee97fb96f..084aed5b4 100644 --- a/packages/backend/src/server/api/mastodon/helpers/pagination.ts +++ b/packages/backend/src/server/api/mastodon/helpers/pagination.ts @@ -45,20 +45,4 @@ export class PaginationHelpers { public static async execQuery(query: SelectQueryBuilder, limit: number, reverse: boolean): Promise { return query.take(limit).getMany().then(found => reverse ? found.reverse() : found); } - - public static appendLinkPaginationHeader(args: any, ctx: any, res: any, defaultLimit: number): void { - const link: string[] = []; - const limit = args.limit ?? defaultLimit; - if (res.maxId) { - const l = `<${config.url}/api${ctx.path}?limit=${limit}&max_id=${convertId(res.maxId, IdType.MastodonId)}>; rel="next"`; - link.push(l); - } - if (res.minId) { - const l = `<${config.url}/api${ctx.path}?limit=${limit}&min_id=${convertId(res.minId, IdType.MastodonId)}>; rel="prev"`; - link.push(l); - } - if (link.length > 0) { - ctx.response.append('Link', link.join(', ')); - } - } } diff --git a/packages/backend/src/server/api/mastodon/helpers/timeline.ts b/packages/backend/src/server/api/mastodon/helpers/timeline.ts index 4be265fda..b9e892da1 100644 --- a/packages/backend/src/server/api/mastodon/helpers/timeline.ts +++ b/packages/backend/src/server/api/mastodon/helpers/timeline.ts @@ -12,12 +12,13 @@ import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/gener import { fetchMeta } from "@/misc/fetch-meta.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { UserList } from "@/models/entities/user-list.js"; -import { LinkPaginationObject, UserHelpers } from "@/server/api/mastodon/helpers/user.js"; +import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { awaitAll } from "@/prelude/await-all.js"; import { unique } from "@/prelude/array.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; export class TimelineHelpers { public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise { @@ -212,8 +213,11 @@ export class TimelineHelpers { }); const res = { data: Promise.all(conversations.map(c => awaitAll(c))), - maxId: p.map(p => p.threadId ?? p.id).at(-1), - minId: p.map(p => p.threadId ?? p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.threadId ?? p.id).at(-1), + minId: p.map(p => p.threadId ?? p.id)[0], + } }; return awaitAll(res); diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 4bc851e9b..1ee875ba8 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -40,6 +40,7 @@ import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js"; import { UserProfile } from "@/models/entities/user-profile.js"; import { verifyLink } from "@/services/fetch-rel-me.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; +import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; export type AccountCache = { locks: AsyncLock; @@ -47,12 +48,6 @@ export type AccountCache = { users: User[]; }; -export type LinkPaginationObject = { - data: T; - maxId?: string | undefined; - minId?: string | undefined; -} - export type updateCredsData = { display_name: string; note: string; @@ -259,8 +254,11 @@ export class UserHelpers { return { data: result, - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } @@ -286,8 +284,11 @@ export class UserHelpers { return { data: users, - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } @@ -313,8 +314,11 @@ export class UserHelpers { return { data: users, - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } @@ -398,8 +402,11 @@ export class UserHelpers { .then(res => { return { data: res.map(p => p.note as Note), - maxId: res.map(p => p.id).at(-1), - minId: res.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: res.map(p => p.id).at(-1), + minId: res.map(p => p.id)[0], + } }; }); } @@ -422,8 +429,11 @@ export class UserHelpers { .then(res => { return { data: res.map(p => p.note as Note), - maxId: res.map(p => p.id).at(-1), - minId: res.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: res.map(p => p.id).at(-1), + minId: res.map(p => p.id)[0], + } }; }); } @@ -467,8 +477,11 @@ export class UserHelpers { return { data: p.map(p => type === "followers" ? p.follower : p.followee).filter(p => p) as User[], - maxId: p.map(p => p.id).at(-1), - minId: p.map(p => p.id)[0], + pagination: { + limit: limit, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + } }; }); } diff --git a/packages/backend/src/server/api/mastodon/index.ts b/packages/backend/src/server/api/mastodon/index.ts index a261fe309..aece784bc 100644 --- a/packages/backend/src/server/api/mastodon/index.ts +++ b/packages/backend/src/server/api/mastodon/index.ts @@ -16,6 +16,7 @@ import { apiLogger } from "@/server/api/logger.js"; import { CacheMiddleware } from "@/server/api/mastodon/middleware/cache.js"; import { KoaBodyMiddleware } from "@/server/api/mastodon/middleware/koa-body.js"; import { NormalizeQueryMiddleware } from "@/server/api/mastodon/middleware/normalize-query.js"; +import { PaginationMiddleware } from "@/server/api/mastodon/middleware/pagination.js"; export const logger = apiLogger.createSubLogger("mastodon"); export type MastoContext = RouterContext & DefaultContext; @@ -36,8 +37,9 @@ export function setupMastodonApi(router: Router): void { function setupMiddleware(router: Router): void { router.use(KoaBodyMiddleware()); + router.use(CatchErrorsMiddleware); router.use(NormalizeQueryMiddleware); + router.use(PaginationMiddleware); router.use(AuthMiddleware); router.use(CacheMiddleware); - router.use(CatchErrorsMiddleware); } diff --git a/packages/backend/src/server/api/mastodon/middleware/pagination.ts b/packages/backend/src/server/api/mastodon/middleware/pagination.ts new file mode 100644 index 000000000..7ae8e3c05 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/middleware/pagination.ts @@ -0,0 +1,33 @@ +import { MastoContext } from "@/server/api/mastodon/index.js"; +import config from "@/config/index.js"; +import { convertId, IdType } from "@/misc/convert-id.js"; + +type PaginationData = { + limit: number; + maxId?: string | undefined; + minId?: string | undefined; +} + +export type LinkPaginationObject = { + data: T; + pagination?: PaginationData; +} + +export async function PaginationMiddleware(ctx: MastoContext, next: () => Promise) { + await next(); + if (!ctx.pagination) return; + + const link: string[] = []; + const limit = ctx.pagination.limit; + if (ctx.pagination.maxId) { + const l = `<${config.url}/api${ctx.path}?limit=${limit}&max_id=${convertId(ctx.pagination.maxId, IdType.MastodonId)}>; rel="next"`; + link.push(l); + } + if (ctx.pagination.minId) { + const l = `<${config.url}/api${ctx.path}?limit=${limit}&min_id=${convertId(ctx.pagination.maxId, IdType.MastodonId)}>; rel="prev"`; + link.push(l); + } + if (link.length > 0) { + ctx.response.append('Link', link.join(', ')); + } +} \ No newline at end of file