diff --git a/packages/backend/src/db/cql.ts b/packages/backend/src/db/cql.ts index e15a4c4036..932b9385cf 100644 --- a/packages/backend/src/db/cql.ts +++ b/packages/backend/src/db/cql.ts @@ -151,10 +151,10 @@ export const scyllaQueries = { ("id", "noteId", "userId", "reaction", "emoji", "createdAt") VALUES (?, ?, ?, ?, ?, ?)`, select: { - byNoteId: `SELECT * FROM reaction_by_id WHERE "noteId" IN ?`, - byUserId: `SELECT * FROM reaction_by_user_id WHERE "userId" IN ?`, + byNoteId: `SELECT * FROM reaction_by_id WHERE "noteId" = ?`, + byUserId: `SELECT * FROM reaction_by_user_id WHERE "userId" = ?`, byNoteAndUser: `SELECT * FROM reaction WHERE "noteId" IN ? AND "userId" IN ?`, - byId: `SELECT * FROM reaction WHERE "id" IN ?`, + byId: `SELECT * FROM reaction WHERE "id" = ?`, }, delete: `DELETE FROM reaction WHERE "noteId" = ? AND "userId" = ?`, }, diff --git a/packages/backend/src/db/scylla.ts b/packages/backend/src/db/scylla.ts index a6274afa1c..d41729eca9 100644 --- a/packages/backend/src/db/scylla.ts +++ b/packages/backend/src/db/scylla.ts @@ -262,7 +262,8 @@ export type FeedType = | "user" | "channel" | "notification" - | "list"; + | "list" + | "reaction"; export function parseScyllaReaction(row: types.Row): ScyllaNoteReaction { return { @@ -310,6 +311,9 @@ export function preparePaginationQuery( case "notification": queryParts.push(prepared.notification.select.byTargetId); break; + case "reaction": + queryParts.push(prepared.reaction.select.byUserId); + break; default: queryParts.push(prepared.note.select.byDate); } @@ -359,17 +363,19 @@ export async function execPaginationQuery( }, filter?: { note?: (_: ScyllaNote[]) => Promise; + reaction?: (_: ScyllaNoteReaction[]) => Promise; notification?: (_: ScyllaNotification[]) => ScyllaNotification[]; }, userId?: User["id"], maxPartitions = config.scylla?.sparseTimelineDays ?? 14, -): Promise { +): Promise { if (!scyllaClient) return []; switch (kind) { case "home": case "user": case "notification": + case "reaction": if (!userId) throw new Error(`Feed ${kind} needs userId`); break; case "renotes": @@ -387,7 +393,7 @@ export async function execPaginationQuery( let { query, untilDate, sinceDate } = preparePaginationQuery(kind, ps); let scannedPartitions = 0; - const found: (ScyllaNote | ScyllaNotification)[] = []; + const found: ScyllaNote[] | ScyllaNotification[] | ScyllaNoteReaction[] = []; const queryLimit = config.scylla?.queryLimit ?? 1000; let foundLimit = ps.limit; if (kind === "list" && ps.userIds) { @@ -399,7 +405,7 @@ export async function execPaginationQuery( const params: (Date | string | string[] | number)[] = []; if (kind === "home" && userId) { params.push(userId, untilDate, untilDate); - } else if (kind === "user" && userId) { + } else if (["user", "reaction"].includes(kind) && userId) { params.push(userId, untilDate); } else if (kind === "renotes" && ps.noteId) { params.push(ps.noteId, untilDate); @@ -429,15 +435,19 @@ export async function execPaginationQuery( if (result.rowLength > 0) { if (kind === "notification") { const notifications = result.rows.map(parseScyllaNotification); - found.push( + (found as ScyllaNotification[]).push( ...(filter?.notification ? filter.notification(notifications) : notifications), ); untilDate = notifications[notifications.length - 1].createdAt; + } else if (kind === "reaction") { + const reactions = result.rows.map(parseScyllaReaction); + (found as ScyllaNoteReaction[]).push(...(filter?.reaction ? await filter.reaction(reactions) : reactions)); + untilDate = reactions[reactions.length - 1].createdAt; } else { const notes = result.rows.map(parseScyllaNote); - found.push(...(filter?.note ? await filter.note(notes) : notes)); + (found as ScyllaNote[]).push(...(filter?.note ? await filter.note(notes) : notes)); untilDate = notes[notes.length - 1].createdAt; } } @@ -454,6 +464,8 @@ export async function execPaginationQuery( if (kind === "notification") { return found as ScyllaNotification[]; + } else if (kind === "reaction") { + return found as ScyllaNoteReaction[]; } return found as ScyllaNote[]; diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts index 6d1dfbd6fd..9597564fc8 100644 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ b/packages/backend/src/models/repositories/note-reaction.ts @@ -39,9 +39,9 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ async packMany( src: NoteReaction[], - me?: { id: User["id"] } | null | undefined, + me?: { id: User["id"] } | null, options?: { - withNote: booleam; + withNote: boolean; }, ): Promise[]> { const reactions = await Promise.allSettled( diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 5936c17446..c87c09d483 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -73,7 +73,7 @@ export default define(meta, paramDef, async (ps, user) => { let reactions: NoteReaction[] = []; if (scyllaClient) { const scyllaQuery = [prepared.reaction.select.byNoteId] - const params: (string | string[] | number)[] = [[ps.noteId]]; + const params: (string | string[] | number)[] = [ps.noteId]; if (ps.type) { scyllaQuery.push(`AND "reaction" = ?`); params.push(query.reaction as string) diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 6b6d32e8ad..3c8dfbc005 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -3,6 +3,15 @@ import define from "../../define.js"; import { makePaginationQuery } from "../../common/make-pagination-query.js"; import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; import { ApiError } from "../../error.js"; +import { + type ScyllaNoteReaction, + execPaginationQuery, + filterVisibility, + parseScyllaNote, + prepared, + scyllaClient, +} from "@/db/scylla.js"; +import { LocalFollowingsCache } from "@/misc/cache.js"; export const meta = { tags: ["users", "reactions"], @@ -49,10 +58,45 @@ export const paramDef = { export default define(meta, paramDef, async (ps, me) => { const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId }); - if (me.id !== ps.userId && !profile.publicReactions) { + if (me?.id !== ps.userId && !profile.publicReactions) { throw new ApiError(meta.errors.reactionsNotPublic); } + if (scyllaClient) { + let followingUserIds: string[] = []; + if (me) { + followingUserIds = await LocalFollowingsCache.init(me.id).then((cache) => + cache.getAll(), + ); + } + + const filter = async (reactions: ScyllaNoteReaction[]) => { + if (!scyllaClient) return reactions; + let noteIds = reactions.map(({ noteId }) => noteId); + if (me) { + const notes = await scyllaClient + .execute(prepared.note.select.byIds, [noteIds], { prepare: true }) + .then((result) => result.rows.map(parseScyllaNote)); + const filteredNoteIds = await filterVisibility( + notes, + me, + followingUserIds, + ).then((notes) => notes.map(({ id }) => id)); + noteIds = noteIds.filter((id) => filteredNoteIds.includes(id)); + } + + return reactions.filter((reaction) => noteIds.includes(reaction.noteId)); + }; + + const foundReactions = (await execPaginationQuery( + "reaction", + ps, + { reaction: filter }, + ps.userId, + )) as ScyllaNoteReaction[]; + return await NoteReactions.packMany(foundReactions, me, { withNote: true }); + } + const query = makePaginationQuery( NoteReactions.createQueryBuilder("reaction"), ps.sinceId,