From 7da7b6e09bbc17c8a353dde0fcc96c5a2e15ca1d Mon Sep 17 00:00:00 2001
From: Laura Hausmann <laura@hausmann.dev>
Date: Sun, 24 Sep 2023 23:39:31 +0200
Subject: [PATCH] [mastodon-client] Proper pagination for /bookmarks &
 /favorites

---
 .../server/api/mastodon/endpoints/account.ts  | 14 ++--
 .../server/api/mastodon/helpers/pagination.ts |  4 +-
 .../src/server/api/mastodon/helpers/user.ts   | 64 +++++++------------
 3 files changed, 34 insertions(+), 48 deletions(-)

diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 07226a8f0..92a8cfd77 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -198,7 +198,7 @@ export function apiAccountMastodon(router: Router): void {
 				const followers = await UserConverter.encodeMany(res.data, cache);
 
 				ctx.body = followers.map((account) => convertAccount(account));
-				PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `accounts/${ctx.params.id}/followers`);
+				PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `v1/accounts/${ctx.params.id}/followers`);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -223,7 +223,7 @@ export function apiAccountMastodon(router: Router): void {
 				const following = await UserConverter.encodeMany(res.data, cache);
 
 				ctx.body = following.map((account) => convertAccount(account));
-				PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `accounts/${ctx.params.id}/following`);
+				PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `v1/accounts/${ctx.params.id}/following`);
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -410,10 +410,11 @@ export function apiAccountMastodon(router: Router): void {
 
 			const cache = UserHelpers.getFreshAccountCache();
 			const args = normalizeUrlQuery(convertTimelinesArgsId(limitToInt(ctx.query as any)));
-			const bookmarks = await UserHelpers.getUserBookmarks(user, args.max_id, args.since_id, args.min_id, args.limit)
-				.then(n => NoteConverter.encodeMany(n, user, cache));
+			const res = await UserHelpers.getUserBookmarks(user, args.max_id, args.since_id, args.min_id, args.limit);
+			const bookmarks = await NoteConverter.encodeMany(res.data, user, cache);
 
 			ctx.body = bookmarks.map(s => convertStatus(s));
+			PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `v1/bookmarks`);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
@@ -433,10 +434,11 @@ export function apiAccountMastodon(router: Router): void {
 
 			const cache = UserHelpers.getFreshAccountCache();
 			const args = normalizeUrlQuery(convertTimelinesArgsId(limitToInt(ctx.query as any)));
-			const favorites = await UserHelpers.getUserFavorites(user, args.max_id, args.since_id, args.min_id, args.limit)
-				.then(n => NoteConverter.encodeMany(n, user, cache));
+			const res = await UserHelpers.getUserFavorites(user, args.max_id, args.since_id, args.min_id, args.limit);
+			const favorites = await NoteConverter.encodeMany(res.data, user, cache);
 
 			ctx.body = favorites.map(s => convertStatus(s));
+			PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, `v1/favourites`);
 		} catch (e: any) {
 			console.error(e);
 			console.error(e.response.data);
diff --git a/packages/backend/src/server/api/mastodon/helpers/pagination.ts b/packages/backend/src/server/api/mastodon/helpers/pagination.ts
index 16f932d88..b99562997 100644
--- a/packages/backend/src/server/api/mastodon/helpers/pagination.ts
+++ b/packages/backend/src/server/api/mastodon/helpers/pagination.ts
@@ -69,11 +69,11 @@ export class PaginationHelpers {
 		const link: string[] = [];
 		const limit = args.limit ?? 40;
 		if (res.maxId) {
-			const l = `<${config.url}/api/v1/${route}?limit=${limit}&max_id=${convertId(res.maxId, IdType.MastodonId)}>; rel="next"`;
+			const l = `<${config.url}/api/${route}?limit=${limit}&max_id=${convertId(res.maxId, IdType.MastodonId)}>; rel="next"`;
 			link.push(l);
 		}
 		if (res.minId) {
-			const l = `<${config.url}/api/v1/${route}?limit=${limit}&min_id=${convertId(res.minId, IdType.MastodonId)}>; rel="prev"`;
+			const l = `<${config.url}/api/${route}?limit=${limit}&min_id=${convertId(res.minId, IdType.MastodonId)}>; rel="prev"`;
 			link.push(l);
 		}
 		if (link.length > 0){
diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts
index 06b529c95..7da05130d 100644
--- a/packages/backend/src/server/api/mastodon/helpers/user.ts
+++ b/packages/backend/src/server/api/mastodon/helpers/user.ts
@@ -78,68 +78,52 @@ export class UserHelpers {
 		return PaginationHelpers.execQuery(query, limit, minId !== undefined);
 	}
 
-	public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
+	public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
 		if (limit > 40) limit = 40;
 
-		const bookmarkQuery = NoteFavorites.createQueryBuilder("favorite")
-			.select("favorite.noteId")
-			.where("favorite.userId = :meId");
-
 		const query = PaginationHelpers.makePaginationQuery(
-			Notes.createQueryBuilder("note"),
+			NoteFavorites.createQueryBuilder("favorite"),
 			sinceId,
 			maxId,
 			minId
 		)
-			.andWhere(`note.id IN (${bookmarkQuery.getQuery()})`)
-			.innerJoinAndSelect("note.user", "user")
-			.leftJoinAndSelect("user.avatar", "avatar")
-			.leftJoinAndSelect("user.banner", "banner")
-			.leftJoinAndSelect("note.reply", "reply")
-			.leftJoinAndSelect("note.renote", "renote")
-			.leftJoinAndSelect("reply.user", "replyUser")
-			.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
-			.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
-			.leftJoinAndSelect("renote.user", "renoteUser")
-			.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
-			.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
+			.andWhere("favorite.userId = :meId", { meId: localUser.id })
+			.leftJoinAndSelect("favorite.note", "note");
 
 		generateVisibilityQuery(query, localUser);
 
-		query.setParameters({ meId: localUser.id });
-		return PaginationHelpers.execQuery(query, limit, minId !== undefined);
+		return PaginationHelpers.execQuery(query, limit, minId !== undefined)
+			.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],
+				};
+			});
 	}
 
-	public static async getUserFavorites(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
+	public static async getUserFavorites(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
 		if (limit > 40) limit = 40;
 
-		const favoriteQuery = NoteReactions.createQueryBuilder("reaction")
-			.select("reaction.noteId")
-			.where("reaction.userId = :meId");
-
 		const query = PaginationHelpers.makePaginationQuery(
-			Notes.createQueryBuilder("note"),
+			NoteReactions.createQueryBuilder("reaction"),
 			sinceId,
 			maxId,
 			minId
 		)
-			.andWhere(`note.id IN (${favoriteQuery.getQuery()})`)
-			.innerJoinAndSelect("note.user", "user")
-			.leftJoinAndSelect("user.avatar", "avatar")
-			.leftJoinAndSelect("user.banner", "banner")
-			.leftJoinAndSelect("note.reply", "reply")
-			.leftJoinAndSelect("note.renote", "renote")
-			.leftJoinAndSelect("reply.user", "replyUser")
-			.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
-			.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
-			.leftJoinAndSelect("renote.user", "renoteUser")
-			.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
-			.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
+			.andWhere("reaction.userId = :meId", { meId: localUser.id })
+			.leftJoinAndSelect("reaction.note", "note");
 
 		generateVisibilityQuery(query, localUser);
 
-		query.setParameters({ meId: localUser.id });
-		return PaginationHelpers.execQuery(query, limit, minId !== undefined);
+		return PaginationHelpers.execQuery(query, limit, minId !== undefined)
+			.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],
+				};
+			});
 	}
 
 	private static async getUserRelationships(type: RelationshipType, user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {