diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 7a2671b09..acf0a7a53 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -399,14 +399,21 @@ export function apiAccountMastodon(router: Router): void { } }); router.get("/v1/bookmarks", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getBookmarks( - convertTimelinesArgsId(limitToInt(ctx.query as any)), - ); - ctx.body = data.data.map((status) => convertStatus(status)); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + 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)); + + ctx.body = bookmarks.map(s => convertStatus(s)); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/helpers/note.ts b/packages/backend/src/server/api/mastodon/helpers/note.ts index a0fa4e67e..126f5e944 100644 --- a/packages/backend/src/server/api/mastodon/helpers/note.ts +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -45,34 +45,4 @@ export class NoteHelpers { return notes.reverse(); } - - /** - * - * @param query - * @param limit - * @param reverse whether the result needs to be .reverse()'d. Set this to true when the parameter minId is not undefined in the original request. - */ - public static async execQuery(query: SelectQueryBuilder, limit: number, reverse: boolean): Promise { - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(limit * 1.5); - let skip = 0; - try { - while (found.length < limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...notes); - skip += take; - if (notes.length < take) break; - } - } catch (error) { - return []; - } - - if (found.length > limit) { - found.length = limit; - } - - return reverse ? found.reverse() : found; - } } diff --git a/packages/backend/src/server/api/mastodon/helpers/pagination.ts b/packages/backend/src/server/api/mastodon/helpers/pagination.ts index 3f116b52d..16f932d88 100644 --- a/packages/backend/src/server/api/mastodon/helpers/pagination.ts +++ b/packages/backend/src/server/api/mastodon/helpers/pagination.ts @@ -7,33 +7,64 @@ export class PaginationHelpers { q: SelectQueryBuilder, sinceId?: string, maxId?: string, - minId?: string + minId?: string, + idField: string = "id" ) { if (sinceId && minId) throw new Error("Can't user both sinceId and minId params"); if (sinceId && maxId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId }); - q.orderBy(`${q.alias}.id`, "DESC"); + q.andWhere(`${q.alias}.${idField} > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId }); + q.orderBy(`${q.alias}.${idField}`, "DESC"); } if (minId && maxId) { - q.andWhere(`${q.alias}.id > :minId`, { minId: minId }); - q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId }); - q.orderBy(`${q.alias}.id`, "ASC"); + q.andWhere(`${q.alias}.${idField} > :minId`, { minId: minId }); + q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId }); + q.orderBy(`${q.alias}.${idField}`, "ASC"); } else if (sinceId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); - q.orderBy(`${q.alias}.id`, "DESC"); + q.andWhere(`${q.alias}.${idField} > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.${idField}`, "DESC"); } else if (minId) { - q.andWhere(`${q.alias}.id > :minId`, { minId: minId }); - q.orderBy(`${q.alias}.id`, "ASC"); + q.andWhere(`${q.alias}.${idField} > :minId`, { minId: minId }); + q.orderBy(`${q.alias}.${idField}`, "ASC"); } else if (maxId) { - q.andWhere(`${q.alias}.id < :maxId`, { maxId: maxId }); - q.orderBy(`${q.alias}.id`, "DESC"); + q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId }); + q.orderBy(`${q.alias}.${idField}`, "DESC"); } else { - q.orderBy(`${q.alias}.id`, "DESC"); + q.orderBy(`${q.alias}.${idField}`, "DESC"); } return q; } + /** + * + * @param query + * @param limit + * @param reverse whether the result needs to be .reverse()'d. Set this to true when the parameter minId is not undefined in the original request. + */ + public static async execQuery(query: SelectQueryBuilder, limit: number, reverse: boolean): Promise { + // We fetch more than requested because some may be filtered out, and if there's less than + // requested, the pagination stops. + const found = []; + const take = Math.floor(limit * 1.5); + let skip = 0; + try { + while (found.length < limit) { + const notes = await query.take(take).skip(skip).getMany(); + found.push(...notes); + skip += take; + if (notes.length < take) break; + } + } catch (error) { + return []; + } + + if (found.length > limit) { + found.length = limit; + } + + return reverse ? found.reverse() : found; + } + public static appendLinkPaginationHeader(args: any, ctx: any, res: any, route: string): void { const link: string[] = []; const limit = args.limit ?? 40; diff --git a/packages/backend/src/server/api/mastodon/helpers/timeline.ts b/packages/backend/src/server/api/mastodon/helpers/timeline.ts index c28ab59ca..85411e0a5 100644 --- a/packages/backend/src/server/api/mastodon/helpers/timeline.ts +++ b/packages/backend/src/server/api/mastodon/helpers/timeline.ts @@ -68,7 +68,7 @@ export class TimelineHelpers { query.andWhere("note.visibility != 'hidden'"); - return NoteHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQuery(query, limit, minId !== undefined); } public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise { @@ -122,6 +122,6 @@ export class TimelineHelpers { query.andWhere("note.visibility != 'hidden'"); - return NoteHelpers.execQuery(query, limit, minId !== undefined); + return PaginationHelpers.execQuery(query, limit, minId !== undefined); } } diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 848d41c7e..89a4c7bbd 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -1,6 +1,6 @@ import { Note } from "@/models/entities/note.js"; import { ILocalUser, User } from "@/models/entities/user.js"; -import { Followings, Notes, UserProfiles } from "@/models/index.js"; +import { Followings, NoteFavorites, Notes, UserProfiles } from "@/models/index.js"; import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; @@ -75,9 +75,42 @@ export class UserHelpers { query.andWhere("note.visibility != 'hidden'"); - return NoteHelpers.execQuery(query, limit, minId !== undefined); + 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 { + if (limit > 40) limit = 40; + + const bookmarkQuery = NoteFavorites.createQueryBuilder("favorite") + .select("favorite.noteId") + .where("favorite.userId = :meId"); + + const query = PaginationHelpers.makePaginationQuery( + Notes.createQueryBuilder("note"), + 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"); + + generateVisibilityQuery(query, localUser); + + query.setParameters({ meId: localUser.id }); + return PaginationHelpers.execQuery(query, limit, minId !== undefined); + } + + private static async getUserRelationships(type: RelationshipType, user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise> { if (limit > 80) limit = 80;