From e1e4160a62c509b4bbe237697d9f45dcf440a0fe Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 15 Sep 2023 13:09:25 +0200 Subject: [PATCH] [mastodon-client] GET /statuses/:id/context --- .../server/api/mastodon/converters/note.ts | 16 +++--- .../server/api/mastodon/endpoints/status.ts | 50 ++++++++----------- .../src/server/api/mastodon/helpers/note.ts | 47 +++++++++++++++++ 3 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 packages/backend/src/server/api/mastodon/helpers/note.ts diff --git a/packages/backend/src/server/api/mastodon/converters/note.ts b/packages/backend/src/server/api/mastodon/converters/note.ts index 297a8dfe3..c09a158a2 100644 --- a/packages/backend/src/server/api/mastodon/converters/note.ts +++ b/packages/backend/src/server/api/mastodon/converters/note.ts @@ -17,10 +17,10 @@ import { populatePoll } from "@/models/repositories/note.js"; import { FileConverter } from "@/server/api/mastodon/converters/file.js"; export class NoteConverter { - public static async encode(note: Note, user: ILocalUser): Promise { + public static async encode(note: Note, user?: ILocalUser): Promise { const noteUser = note.user ?? await getUser(note.userId); - if (!await Notes.isVisibleForMe(note, user.id ?? null)) + if (!await Notes.isVisibleForMe(note, user?.id ?? null)) throw new Error(); const host = note.user?.host ?? null; @@ -49,22 +49,22 @@ export class NoteConverter { } }) : null; - const reply = note.reply ?? (note.replyId ? await getNote(note.replyId, user) : null); + const reply = note.reply ?? (note.replyId ? await getNote(note.replyId, user ?? null) : null); - const isBookmarked = await NoteFavorites.exist({ + const isBookmarked = user ? await NoteFavorites.exist({ where: { userId: user.id, noteId: note.id, }, take: 1, - }); + }) : false; - const isMuted = await NoteThreadMutings.exist({ + const isMuted = user ? await NoteThreadMutings.exist({ where: { userId: user.id, threadId: note.threadId || note.id, } - }); + }) : false; const files = await DriveFiles.packMany(note.fileIds); @@ -98,7 +98,7 @@ export class NoteConverter { mentions: await Promise.all(note.mentions.map(async p => MentionConverter.encode(await getUser(p)))), tags: [], //FIXME card: null, //FIXME - poll: note.hasPoll ? PollConverter.encode(await populatePoll(note, user.id), note.id) : null, + poll: note.hasPoll ? PollConverter.encode(await populatePoll(note, user?.id ?? null), note.id) : null, application: null, //FIXME language: null, //FIXME pinned: null, //FIXME diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 0532bda68..ab375f90d 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -4,18 +4,12 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import axios from "axios"; import querystring from "node:querystring"; import qs from "qs"; -import { convertTimelinesArgsId, limitToInt } from "./timeline.js"; import { convertId, IdType } from "../../index.js"; -import { - convertAccount, - convertAttachment, - convertPoll, - convertStatus, -} from "../converters.js"; +import { convertAccount, convertAttachment, convertPoll, convertStatus, } from "../converters.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { getNote } from "@/server/api/common/getters.js"; import authenticate from "@/server/api/authenticate.js"; -import {Notes} from "@/models"; +import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js"; function normalizeQuery(data: any) { const str = querystring.stringify(data); @@ -153,15 +147,10 @@ export function apiStatusMastodon(router: Router): void { router.get<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => { try { const auth = await authenticate(ctx.headers.authorization, null); - const user = auth[0]; - - if (!auth || !user) { - ctx.status = 401; - return; - } + const user = auth[0] ?? undefined; const noteId = convertId(ctx.params.id, IdType.IceshrimpId); - const note = await getNote(noteId, user).then(n => n).catch(() => null); + const note = await getNote(noteId, user ?? null).then(n => n).catch(() => null); if (!note) { ctx.status = 404; @@ -200,23 +189,26 @@ export function apiStatusMastodon(router: Router): void { router.get<{ Params: { id: string } }>( "/v1/statuses/:id/context", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const id = convertId(ctx.params.id, IdType.IceshrimpId); - const data = await client.getStatusContext( - id, - convertTimelinesArgsId(limitToInt(ctx.query as any)), - ); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? undefined; - data.data.ancestors = data.data.ancestors.map((status) => - convertStatus(status), - ); - data.data.descendants = data.data.descendants.map((status) => - convertStatus(status), - ); - ctx.body = data.data; + const id = convertId(ctx.params.id, IdType.IceshrimpId); + const note = await getNote(id, user ?? null).then(n => n).catch(() => null); + if (!note) { + if (!note) { + ctx.status = 404; + return; + } + } + + let ancestors = await NoteHelpers.getNoteAncestors(note, user, user ? 4096 : 60); + let children = await NoteHelpers.getNoteChildren(note, user, user ? 4096 : 40, user ? 4096 : 20); + ctx.body = { + ancestors: (await Promise.all(ancestors.map(n => NoteConverter.encode(n, user)))).map(s => convertStatus(s)), + descendants: (await Promise.all(children.map(n => NoteConverter.encode(n, user)))).map(s => convertStatus(s)), + }; } 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 new file mode 100644 index 000000000..e46796e35 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -0,0 +1,47 @@ +import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; +import { Notes } 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 querystring from "node:querystring"; +import { getNote } from "@/server/api/common/getters.js"; + +export class NoteHelpers { + public static async getNoteChildren(note: Note | string, user?: ILocalUser, limit: number = 10, depth: number = 2): Promise { + const noteId = typeof note === "string" ? note : note.id; + const query = makePaginationQuery(Notes.createQueryBuilder("note")) + .andWhere( + "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", + {noteId, depth, limit}, + ) + .innerJoinAndSelect("note.user", "user") + .leftJoinAndSelect("user.avatar", "avatar") + .leftJoinAndSelect("user.banner", "banner"); + + generateVisibilityQuery(query, user); + if (user) { + generateMutedUserQuery(query, user); + generateBlockedUserQuery(query, user); + } + + return query.getMany(); + } + + public static async getNoteAncestors(rootNote: Note, user?: ILocalUser, limit: number = 10): Promise { + const notes = new Array; + for (let i = 0; i < limit; i++) { + const currentNote = notes.at(-1) ?? rootNote; + if (!currentNote.replyId) break; + const nextNote = await getNote(currentNote.replyId, user ?? null).catch((e) => { + if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null; + throw e; + }); + if (nextNote) notes.push(nextNote); + else break; + } + + return notes; + } +}