diff --git a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql index cc1f948761..5d48bbcc0d 100644 --- a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql +++ b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql @@ -9,6 +9,7 @@ DROP MATERIALIZED VIEW IF EXISTS global_timeline; DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id_and_user_id; DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id; DROP MATERIALIZED VIEW IF EXISTS note_by_user_id; +DROP INDEX IF EXISTS note_by_reply_id; DROP INDEX IF EXISTS note_by_id; DROP INDEX IF EXISTS note_by_uri; DROP INDEX IF EXISTS note_by_url; diff --git a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql index 080a85cba4..50c952e6ec 100644 --- a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql +++ b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql @@ -75,6 +75,7 @@ CREATE TABLE IF NOT EXISTS note ( -- Store all posts CREATE INDEX IF NOT EXISTS note_by_uri ON note ("uri"); CREATE INDEX IF NOT EXISTS note_by_url ON note ("url"); CREATE INDEX IF NOT EXISTS note_by_id ON note ("id"); +CREATE INDEX IF NOT EXISTS note_by_reply_id ON note ("replyId"); CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_user_id AS SELECT * FROM note diff --git a/packages/backend/src/db/cql.ts b/packages/backend/src/db/cql.ts index 366c5a6dc2..c807e322d7 100644 --- a/packages/backend/src/db/cql.ts +++ b/packages/backend/src/db/cql.ts @@ -48,6 +48,7 @@ export const scyllaQueries = { byId: `SELECT * FROM note WHERE "id" = ?`, byUserId: `SELECT * FROM note_by_user_id WHERE "userId" IN ?`, byRenoteId: `SELECT * FROM note_by_renote_id WHERE "renoteId" = ?`, + byReplyId: `SELECT * FROM note_by_reply_id WHERE "replyId" = ?` }, delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "userId" = ? AND "userHost" = ? AND "visibility" = ?`, update: { diff --git a/packages/backend/src/db/scylla.ts b/packages/backend/src/db/scylla.ts index 76b09b0156..0cfd098cd3 100644 --- a/packages/backend/src/db/scylla.ts +++ b/packages/backend/src/db/scylla.ts @@ -82,6 +82,24 @@ export interface ScyllaDriveFile { height: number | null; } +export function getScyllaDrivePublicUrl(file: ScyllaDriveFile, thumbnail = false): string | null { + const isImage = + file.type && + [ + "image/png", + "image/apng", + "image/gif", + "image/jpeg", + "image/webp", + "image/svg+xml", + "image/avif", + ].includes(file.type); + + return thumbnail + ? file.thumbnailUrl || (isImage ? file.url : null) + : file.url; +} + export interface ScyllaNoteEditHistory { content: string; cw: string; diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 3918f7947b..9c319e2a49 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -10,6 +10,7 @@ import { Meta } from "@/models/entities/meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { Users, DriveFolders } from "../index.js"; import { deepClone } from "@/misc/clone.js"; +import { ScyllaDriveFile } from "@/db/scylla.js"; type PackOptions = { detail?: boolean; diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 1aef1cdef9..5e8b5ca480 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -33,6 +33,7 @@ import { prepared, scyllaClient, parseScyllaReaction, + getScyllaDrivePublicUrl, } from "@/db/scylla.js"; import { LocalFollowingsCache } from "@/misc/cache.js"; import { userByIdCache } from "@/services/user-cache.js"; @@ -280,6 +281,7 @@ export const NoteRepository = db.getRepository(Note).extend({ files: scyllaClient ? (note as ScyllaNote).files.map((file) => ({ ...file, + thumbnailUrl: getScyllaDrivePublicUrl(file, true), createdAt: file.createdAt.toISOString(), properties: { width: file.width ?? undefined, diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts index a35b17a022..1c1a496f41 100644 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ b/packages/backend/src/server/api/endpoints/notes/children.ts @@ -4,6 +4,9 @@ import { makePaginationQuery } from "../../common/make-pagination-query.js"; import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; +import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js"; +import { getNote } from "@/server/api/common/getters.js"; +import type { Note } from "@/models/entities/note.js"; export const meta = { tags: ["notes"], @@ -38,18 +41,52 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + if (scyllaClient) { + const root = await getNote(ps.noteId, user).catch(() => null); + if (!root) { + return await Notes.packMany([]); + } + + // Find replies in BFS manner + const queue = [root]; + const foundReplies: Note[] = []; + let depth = 0; + + while ( + queue.length > 0 && + foundReplies.length < ps.limit && + depth < ps.depth + ) { + const note = queue.shift(); + if (note) { + const result = await scyllaClient.execute( + prepared.note.select.byReplyId, + [note.id], + { prepare: true }, + ); + if (result.rowLength > 0) { + const replies = result.rows.map(parseScyllaNote); + foundReplies.push(...replies); + queue.push(...replies); + depth++; + } + } + } + + return await Notes.packMany(foundReplies.slice(0, ps.limit), user, { + detail: false, + scyllaNote: true, + }); + } + const query = makePaginationQuery( Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId, - ) - .andWhere( - "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", - { noteId: ps.noteId, depth: ps.depth, limit: ps.limit }, - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner"); + ).andWhere( + "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", + { noteId: ps.noteId, depth: ps.depth, limit: ps.limit }, + ); generateVisibilityQuery(query, user); if (user) { diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 563b1fb887..9813ef9c15 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -148,7 +148,7 @@ export default async function ( deletedAt: deletedAt, }); - //#region ローカルの投稿なら削除アクティビティを配送 + //#region Deliver Delete activity if it's from a local account if (Users.isLocalUser(user) && !note.localOnly) { let renote: Note | null = null; @@ -159,9 +159,18 @@ export default async function ( !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0) ) { - renote = await Notes.findOneBy({ - id: note.renoteId, - }); + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [note.renoteId], + { prepare: true }, + ); + if (result.rowLength > 0) renote = parseScyllaNote(result.first()); + } else { + renote = await Notes.findOneBy({ + id: note.renoteId, + }); + } } const content = renderActivity( @@ -249,18 +258,42 @@ async function findCascadingNotes(note: Note) { const cascadingNotes: Note[] = []; const recursive = async (noteId: string) => { - const query = Notes.createQueryBuilder("note") - .where("note.replyId = :noteId", { noteId }) - .orWhere( - new Brackets((q) => { - q.where("note.renoteId = :noteId", { noteId }).andWhere( - "note.text IS NOT NULL", - ); - }), - ) - .leftJoinAndSelect("note.user", "user"); - const replies = await query.getMany(); - for (const reply of replies) { + let notes: Note[] = []; + + if (scyllaClient) { + const replies = await scyllaClient.execute( + prepared.note.select.byReplyId, + [noteId], + { prepare: true }, + ); + if (replies.rowLength > 0) { + notes.push(...replies.rows.map(parseScyllaNote)); + } + const renotes = await scyllaClient.execute( + prepared.note.select.byRenoteId, + [noteId], + { prepare: true }, + ); + if (renotes.rowLength > 0) { + notes.push( + ...renotes.rows.map(parseScyllaNote).filter((note) => !!note.text), + ); + } + } else { + const query = Notes.createQueryBuilder("note") + .where("note.replyId = :noteId", { noteId }) + .orWhere( + new Brackets((q) => { + q.where("note.renoteId = :noteId", { noteId }).andWhere( + "note.text IS NOT NULL", + ); + }), + ) + .leftJoinAndSelect("note.user", "user"); + notes = await query.getMany(); + } + + for (const reply of notes) { cascadingNotes.push(reply); await recursive(reply.id); }