[mastodon-client] Move link header pagination to middleware

This commit is contained in:
Laura Hausmann 2023-10-06 21:59:24 +02:00
parent 3d320c0895
commit 081b836e92
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
11 changed files with 98 additions and 55 deletions

View file

@ -81,7 +81,7 @@ export function setupEndpointsAccount(router: Router): void {
const followers = await UserConverter.encodeMany(res.data, ctx.cache); const followers = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = followers.map((account) => convertAccountId(account)); ctx.body = followers.map((account) => convertAccountId(account));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
}, },
); );
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
@ -95,7 +95,7 @@ export function setupEndpointsAccount(router: Router): void {
const following = await UserConverter.encodeMany(res.data, ctx.cache); const following = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = following.map((account) => convertAccountId(account)); ctx.body = following.map((account) => convertAccountId(account));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
}, },
); );
router.get<{ Params: { id: string } }>( 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 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); const bookmarks = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache);
ctx.body = bookmarks.map(s => convertStatusIds(s)); ctx.body = bookmarks.map(s => convertStatusIds(s));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); ctx.pagination = res.pagination;
} }
); );
router.get("/v1/favourites", 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 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); const favorites = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache);
ctx.body = favorites.map(s => convertStatusIds(s)); ctx.body = favorites.map(s => convertStatusIds(s));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); ctx.pagination = res.pagination;
} }
); );
router.get("/v1/mutes", router.get("/v1/mutes",
@ -200,7 +200,7 @@ export function setupEndpointsAccount(router: Router): void {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); 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); 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)); ctx.body = res.data.map(m => convertAccountId(m));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
} }
); );
router.get("/v1/blocks", 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 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); const blocks = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = blocks.map(b => convertAccountId(b)); ctx.body = blocks.map(b => convertAccountId(b));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
} }
); );
router.get("/v1/follow_requests", 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 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); const requests = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = requests.map(b => convertAccountId(b)); ctx.body = requests.map(b => convertAccountId(b));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
} }
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(

View file

@ -75,7 +75,7 @@ export function setupEndpointsList(router: Router): void {
const accounts = await UserConverter.encodeMany(res.data); const accounts = await UserConverter.encodeMany(res.data);
ctx.body = accounts.map(account => convertAccountId(account)); ctx.body = accounts.map(account => convertAccountId(account));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
}, },
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(

View file

@ -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 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); const users = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = users.map(m => convertAccountId(m)); ctx.body = users.map(m => convertAccountId(m));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
} }
); );
router.get<{ Params: { id: string } }>( 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 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); const users = await UserConverter.encodeMany(res.data, ctx.cache);
ctx.body = users.map(m => convertAccountId(m)); ctx.body = users.map(m => convertAccountId(m));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40); ctx.pagination = res.pagination;
} }
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(

View file

@ -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); 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)); ctx.body = res.data.map(c => convertConversationIds(c));
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20); ctx.pagination = res.pagination;
} }
); );
} }

View file

@ -1,6 +1,6 @@
import { ILocalUser, User } from "@/models/entities/user.js"; import { ILocalUser, User } from "@/models/entities/user.js";
import { Blockings, UserListJoinings, UserLists, Users } from "@/models/index.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 { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { UserList } from "@/models/entities/user-list.js"; import { UserList } from "@/models/entities/user-list.js";
import { pushUserToUserList } from "@/services/user-list/push.js"; import { pushUserToUserList } from "@/services/user-list/push.js";
@ -54,8 +54,11 @@ export class ListHelpers {
return { return {
data: users, data: users,
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }

View file

@ -14,7 +14,8 @@ import deleteNote from "@/services/note/delete.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { UserConverter } from "@/server/api/mastodon/converters/user.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 { addPinned, removePinned } from "@/services/i/pin.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { convertId, IdType } from "@/misc/convert-id.js"; import { convertId, IdType } from "@/misc/convert-id.js";
@ -158,8 +159,11 @@ export class NoteHelpers {
return { return {
data: users, data: users,
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }

View file

@ -45,20 +45,4 @@ export class PaginationHelpers {
public static async execQuery<T extends ObjectLiteral>(query: SelectQueryBuilder<T>, limit: number, reverse: boolean): Promise<T[]> { public static async execQuery<T extends ObjectLiteral>(query: SelectQueryBuilder<T>, limit: number, reverse: boolean): Promise<T[]> {
return query.take(limit).getMany().then(found => reverse ? found.reverse() : found); 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(', '));
}
}
} }

View file

@ -12,12 +12,13 @@ import { generateMutedUserRenotesQueryForNotes } from "@/server/api/common/gener
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { UserList } from "@/models/entities/user-list.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 { UserConverter } from "@/server/api/mastodon/converters/user.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { awaitAll } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js";
import { unique } from "@/prelude/array.js"; import { unique } from "@/prelude/array.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js";
export class TimelineHelpers { export class TimelineHelpers {
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> { public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
@ -212,8 +213,11 @@ export class TimelineHelpers {
}); });
const res = { const res = {
data: Promise.all(conversations.map(c => awaitAll(c))), data: Promise.all(conversations.map(c => awaitAll(c))),
pagination: {
limit: limit,
maxId: p.map(p => p.threadId ?? p.id).at(-1), maxId: p.map(p => p.threadId ?? p.id).at(-1),
minId: p.map(p => p.threadId ?? p.id)[0], minId: p.map(p => p.threadId ?? p.id)[0],
}
}; };
return awaitAll(res); return awaitAll(res);

View file

@ -40,6 +40,7 @@ import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js";
import { UserProfile } from "@/models/entities/user-profile.js"; import { UserProfile } from "@/models/entities/user-profile.js";
import { verifyLink } from "@/services/fetch-rel-me.js"; import { verifyLink } from "@/services/fetch-rel-me.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js";
export type AccountCache = { export type AccountCache = {
locks: AsyncLock; locks: AsyncLock;
@ -47,12 +48,6 @@ export type AccountCache = {
users: User[]; users: User[];
}; };
export type LinkPaginationObject<T> = {
data: T;
maxId?: string | undefined;
minId?: string | undefined;
}
export type updateCredsData = { export type updateCredsData = {
display_name: string; display_name: string;
note: string; note: string;
@ -259,8 +254,11 @@ export class UserHelpers {
return { return {
data: result, data: result,
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }
@ -286,8 +284,11 @@ export class UserHelpers {
return { return {
data: users, data: users,
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }
@ -313,8 +314,11 @@ export class UserHelpers {
return { return {
data: users, data: users,
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }
@ -398,8 +402,11 @@ export class UserHelpers {
.then(res => { .then(res => {
return { return {
data: res.map(p => p.note as Note), data: res.map(p => p.note as Note),
pagination: {
limit: limit,
maxId: res.map(p => p.id).at(-1), maxId: res.map(p => p.id).at(-1),
minId: res.map(p => p.id)[0], minId: res.map(p => p.id)[0],
}
}; };
}); });
} }
@ -422,8 +429,11 @@ export class UserHelpers {
.then(res => { .then(res => {
return { return {
data: res.map(p => p.note as Note), data: res.map(p => p.note as Note),
pagination: {
limit: limit,
maxId: res.map(p => p.id).at(-1), maxId: res.map(p => p.id).at(-1),
minId: res.map(p => p.id)[0], minId: res.map(p => p.id)[0],
}
}; };
}); });
} }
@ -467,8 +477,11 @@ export class UserHelpers {
return { return {
data: p.map(p => type === "followers" ? p.follower : p.followee).filter(p => p) as User[], data: p.map(p => type === "followers" ? p.follower : p.followee).filter(p => p) as User[],
pagination: {
limit: limit,
maxId: p.map(p => p.id).at(-1), maxId: p.map(p => p.id).at(-1),
minId: p.map(p => p.id)[0], minId: p.map(p => p.id)[0],
}
}; };
}); });
} }

View file

@ -16,6 +16,7 @@ import { apiLogger } from "@/server/api/logger.js";
import { CacheMiddleware } from "@/server/api/mastodon/middleware/cache.js"; import { CacheMiddleware } from "@/server/api/mastodon/middleware/cache.js";
import { KoaBodyMiddleware } from "@/server/api/mastodon/middleware/koa-body.js"; import { KoaBodyMiddleware } from "@/server/api/mastodon/middleware/koa-body.js";
import { NormalizeQueryMiddleware } from "@/server/api/mastodon/middleware/normalize-query.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 const logger = apiLogger.createSubLogger("mastodon");
export type MastoContext = RouterContext & DefaultContext; export type MastoContext = RouterContext & DefaultContext;
@ -36,8 +37,9 @@ export function setupMastodonApi(router: Router): void {
function setupMiddleware(router: Router): void { function setupMiddleware(router: Router): void {
router.use(KoaBodyMiddleware()); router.use(KoaBodyMiddleware());
router.use(CatchErrorsMiddleware);
router.use(NormalizeQueryMiddleware); router.use(NormalizeQueryMiddleware);
router.use(PaginationMiddleware);
router.use(AuthMiddleware); router.use(AuthMiddleware);
router.use(CacheMiddleware); router.use(CacheMiddleware);
router.use(CatchErrorsMiddleware);
} }

View file

@ -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<T> = {
data: T;
pagination?: PaginationData;
}
export async function PaginationMiddleware(ctx: MastoContext, next: () => Promise<any>) {
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(', '));
}
}