diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index e2095a2bf..b4dd3a904 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -13,6 +13,9 @@ import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import createReaction from "@/services/note/reaction/create.js"; import deleteReaction from "@/services/note/reaction/delete.js"; +import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js"; +import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; +import { UserConverter } from "@/server/api/mastodon/converters/user.js"; function normalizeQuery(data: any) { const str = querystring.stringify(data); @@ -251,10 +254,28 @@ export function apiStatusMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatusRebloggedBy( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - ctx.body = data.data.map((account) => convertAccount(account)); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const id = convertId(ctx.params.id, IdType.IceshrimpId); + const note = await getNote(id, user).catch(_ => null); + + if (note === null) { + ctx.status = 404; + return; + } + + const cache = UserHelpers.getFreshAccountCache(); + const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); + const res = await NoteHelpers.getNoteRebloggedBy(note, args.max_id, args.since_id, args.min_id, args.limit); + const users = await UserConverter.encodeMany(res.data, cache); + ctx.body = users.map(m => convertAccount(m)); + PaginationHelpers.appendLinkPaginationHeader(args, ctx, res); } catch (e: any) { console.error(e); ctx.status = 401; @@ -265,14 +286,29 @@ export function apiStatusMastodon(router: Router): void { router.get<{ Params: { id: string } }>( "/v1/statuses/:id/favourited_by", 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.getStatusFavouritedBy( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - ctx.body = data.data.map((account) => convertAccount(account)); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const id = convertId(ctx.params.id, IdType.IceshrimpId); + const note = await getNote(id, user).catch(_ => null); + + if (note === null) { + ctx.status = 404; + return; + } + + const cache = UserHelpers.getFreshAccountCache(); + const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); + const res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit); + const users = await UserConverter.encodeMany(res.data, cache); + ctx.body = users.map(m => convertAccount(m)); + PaginationHelpers.appendLinkPaginationHeader(args, ctx, res); } catch (e: any) { console.error(e); ctx.status = 401; diff --git a/packages/backend/src/server/api/mastodon/helpers/note.ts b/packages/backend/src/server/api/mastodon/helpers/note.ts index fa69afba7..058c37981 100644 --- a/packages/backend/src/server/api/mastodon/helpers/note.ts +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -1,16 +1,19 @@ import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; -import { Metas, NoteFavorites, Notes, Users } from "@/models/index.js"; +import { Metas, NoteFavorites, NoteReactions, Notes, Users } from "@/models/index.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; import { Note } from "@/models/entities/note.js"; -import { ILocalUser } from "@/models/entities/user.js"; +import { ILocalUser, User } from "@/models/entities/user.js"; import { getNote } from "@/server/api/common/getters.js"; import createReaction from "@/services/note/reaction/create.js"; import deleteReaction from "@/services/note/reaction/delete.js"; import createNote from "@/services/note/create.js"; 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 { AccountCache, LinkPaginationObject, UserHelpers } from "@/server/api/mastodon/helpers/user.js"; export class NoteHelpers { public static async getDefaultReaction(): Promise { @@ -70,7 +73,7 @@ export class NoteHelpers { } public static async unbookmarkNote(note: Note, user: ILocalUser): Promise { - return await NoteFavorites.findOneBy({ + return NoteFavorites.findOneBy({ noteId: note.id, userId: user.id, }) @@ -78,6 +81,57 @@ export class NoteHelpers { .then(_ => note); } + public static async getNoteFavoritedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise> { + if (limit > 80) limit = 80; + const query = PaginationHelpers.makePaginationQuery( + NoteReactions.createQueryBuilder("reaction"), + sinceId, + maxId, + minId + ) + .andWhere("reaction.noteId = :noteId", {noteId: note.id}) + .innerJoinAndSelect("reaction.user", "user"); + + return query.take(limit).getMany().then(async p => { + if (minId !== undefined) p = p.reverse(); + const users = p + .map(p => p.user) + .filter(p => p) as User[]; + + return { + data: users, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + }; + }); + } + + public static async getNoteRebloggedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise> { + if (limit > 80) limit = 80; + const query = PaginationHelpers.makePaginationQuery( + Notes.createQueryBuilder("note"), + sinceId, + maxId, + minId + ) + .andWhere("note.renoteId = :noteId", {noteId: note.id}) + .andWhere("note.text IS NULL") // We don't want to count quotes as renotes + .innerJoinAndSelect("note.user", "user"); + + return query.take(limit).getMany().then(async p => { + if (minId !== undefined) p = p.reverse(); + const users = p + .map(p => p.user) + .filter(p => p) as User[]; + + return { + data: users, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + }; + }); + } + public static async getNoteDescendants(note: Note | string, user: ILocalUser | null, limit: number = 10, depth: number = 2): Promise { const noteId = typeof note === "string" ? note : note.id; const query = makePaginationQuery(Notes.createQueryBuilder("note"))