diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 2ad2fec9fb..a71325d4ef 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -10,6 +10,16 @@ import renderEmoji from "./emoji.js"; import renderMention from "./mention.js"; import renderHashtag from "./hashtag.js"; import renderDocument from "./document.js"; +import { + type ScyllaPoll, + type ScyllaNote, + parseScyllaNote, + prepared, + scyllaClient, + parseScyllaPollVote, +} from "@/db/scylla.js"; +import { userByIdCache } from "@/services/user-cache.js"; +import { EmojiCache } from "@/misc/populate-emojis.js"; export default async function renderNote( note: Note, @@ -25,15 +35,32 @@ export default async function renderNote( }; let inReplyTo; - let inReplyToNote: Note | null; + let inReplyToNote: Note | null = null; if (note.replyId) { - inReplyToNote = await Notes.findOneBy({ id: note.replyId }); + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [note.replyId], + { prepare: true }, + ); + if (result.rowLength > 0) { + inReplyToNote = parseScyllaNote(result.first()); + } + } else { + inReplyToNote = await Notes.findOneBy({ id: note.replyId }); + } - if (inReplyToNote != null) { - const inReplyToUser = await Users.findOneBy({ id: inReplyToNote.userId }); + if (inReplyToNote) { + const inReplyToUser = await userByIdCache.fetchMaybe( + inReplyToNote.userId, + () => + Users.findOneBy({ id: (inReplyToNote as Note).userId }).then( + (user) => user ?? undefined, + ), + ); - if (inReplyToUser != null) { + if (!inReplyToUser) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { @@ -52,7 +79,19 @@ export default async function renderNote( let quote; if (note.renoteId) { - const renote = await Notes.findOneBy({ id: note.renoteId }); + let renote: Note | null = null; + 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 }); + } if (renote) { quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; @@ -94,10 +133,14 @@ export default async function renderNote( const files = await getPromisedFiles(note.fileIds); const text = note.text ?? ""; - let poll: Poll | null = null; + let poll: Poll | ScyllaPoll | null = null; if (note.hasPoll) { - poll = await Polls.findOneBy({ noteId: note.id }); + if (scyllaClient) { + poll = (note as ScyllaNote).poll; + } else { + poll = await Polls.findOneBy({ noteId: note.id }); + } } let apText = text; @@ -119,6 +162,39 @@ export default async function renderNote( const tag = [...hashtagTags, ...mentionTags, ...apemojis]; + let choices: { + type: "Note"; + name: string; + replies: { type: "Collection"; totalItems: number }; + }[] = []; + if (poll) { + if (scyllaClient) { + const votes = await scyllaClient + .execute(prepared.poll.select, [note.id], { prepare: true }) + .then((result) => result.rows.map(parseScyllaPollVote)); + choices = Object.entries((poll as ScyllaPoll).choices).map( + ([index, text]) => ({ + type: "Note", + name: text, + replies: { + type: "Collection", + totalItems: votes.filter((vote) => vote.choice.has(parseInt(index))) + .length, + }, + }), + ); + } else { + choices = (poll as Poll).choices.map((text, i) => ({ + type: "Note", + name: text, + replies: { + type: "Collection", + totalItems: (poll as Poll).votes[i], + }, + })); + } + } + const asPoll = poll ? { type: "Question", @@ -129,14 +205,7 @@ export default async function renderNote( ), [poll.expiresAt && poll.expiresAt < new Date() ? "closed" : "endTime"]: poll.expiresAt, - [poll.multiple ? "anyOf" : "oneOf"]: poll.choices.map((text, i) => ({ - type: "Note", - name: text, - replies: { - type: "Collection", - totalItems: poll!.votes[i], - }, - })), + [poll.multiple ? "anyOf" : "oneOf"]: choices, } : {}; @@ -177,12 +246,14 @@ export async function getEmojis(names: string[]): Promise { const emojis = await Promise.all( names.map((name) => - Emojis.findOneBy({ - name, - host: IsNull(), - }), + EmojiCache.fetch(`${name} null`, () => + Emojis.findOneBy({ + name, + host: IsNull(), + }), + ), ), ); - return emojis.filter((emoji) => emoji != null) as Emoji[]; + return emojis.filter((emoji) => !!emoji) as Emoji[]; } diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 5ce2653887..4dd6b7f0e1 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -1,6 +1,12 @@ import { Notes } from "@/models/index.js"; import define from "../define.js"; import { makePaginationQuery } from "../common/make-pagination-query.js"; +import { + type ScyllaNote, + scyllaClient, + execPaginationQuery, + FeedType, +} from "@/db/scylla.js"; export const meta = { tags: ["notes"], @@ -35,6 +41,43 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps) => { + if (scyllaClient) { + const filter = async (notes: ScyllaNote[]) => { + let filtered = notes.filter((note) => !note.localOnly); + if (ps.reply === undefined) { + filtered = filtered.filter( + (note) => !!note.replyId === (ps.reply as boolean), + ); + } + if (ps.renote === undefined) { + filtered = filtered.filter( + (note) => !!note.renoteId === (ps.renote as boolean), + ); + } + if (ps.withFiles === undefined) { + filtered = filtered.filter((note) => + ps.withFiles ? note.files.length > 0 : note.files.length === 0, + ); + } + if (ps.poll === undefined) { + filtered = filtered.filter( + (note) => note.hasPoll === (ps.poll as boolean), + ); + } + + return filtered; + }; + + const foundNotes = (await execPaginationQuery( + ps.local ? "global" : "local", + ps, + { + note: filter, + }, + )) as ScyllaNote[]; + return await Notes.packMany(foundNotes); + } + const query = makePaginationQuery( Notes.createQueryBuilder("note"), ps.sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index f5a8f9df07..0e0ecfb261 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -116,28 +116,6 @@ export default define(meta, paramDef, async (ps, user) => { ); } - publishNoteStream(scyllaNote.id, "pollVoted", { - choice: ps.choice, - userId: user.id, - }); - createNotification(scyllaNote.userId, "pollVote", { - notifierId: user.id, - noteId: scyllaNote.id, - choice: ps.choice, - }); - NoteWatchings.findBy({ - noteId: scyllaNote.id, - userId: Not(user.id), - }).then((watchers) => { - for (const watcher of watchers) { - createNotification(watcher.userId, "pollVote", { - notifierId: user.id, - noteId: scyllaNote.id, - choice: ps.choice, - }); - } - }); - return; } diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 7f28f443f5..22f9bb16e7 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -35,6 +35,9 @@ import { urlPreviewHandler } from "./url-preview.js"; import { manifestHandler } from "./manifest.js"; import packFeed from "./feed.js"; import { MINUTE, DAY } from "@/const.js"; +import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js"; +import { userByIdCache } from "@/services/user-cache.js"; +import type { Note } from "@/models/entities/note.js"; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -465,10 +468,25 @@ router.get("/users/:user", async (ctx) => { // Note router.get("/notes/:note", async (ctx, next) => { - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(["public", "home"]), - }); + let note: Note | null = null; + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [ctx.params.note], + { prepare: true }, + ); + if (result.rowLength > 0) { + const candidate = parseScyllaNote(result.first()); + if (["public", "home"].includes(candidate.visibility)) { + note = candidate; + } + } + } else { + note = await Notes.findOneBy({ + id: ctx.params.note, + visibility: In(["public", "home"]), + }); + } try { if (note) { @@ -483,7 +501,9 @@ router.get("/notes/:note", async (ctx, next) => { note: _note, profile, avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: note.userId }), + await userByIdCache.fetch(note.userId, () => + Users.findOneByOrFail({ id: (note as Note).userId }), + ), ), // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), @@ -503,10 +523,25 @@ router.get("/notes/:note", async (ctx, next) => { }); router.get("/posts/:note", async (ctx, next) => { - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(["public", "home"]), - }); + let note: Note | null = null; + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [ctx.params.note], + { prepare: true }, + ); + if (result.rowLength > 0) { + const candidate = parseScyllaNote(result.first()); + if (["public", "home"].includes(candidate.visibility)) { + note = candidate; + } + } + } else { + note = await Notes.findOneBy({ + id: ctx.params.note, + visibility: In(["public", "home"]), + }); + } if (note) { const _note = await Notes.pack(note); @@ -517,7 +552,9 @@ router.get("/posts/:note", async (ctx, next) => { note: _note, profile, avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: note.userId }), + await userByIdCache.fetch(note.userId, () => + Users.findOneByOrFail({ id: (note as Note).userId }), + ), ), // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index cc73f3c1ed..14adf95c5c 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -140,6 +140,11 @@ export async function createNotification( // Fire "new notification" event if not yet read after two seconds setTimeout(async () => { + if (scyllaClient) { + pushNotification(notifieeId, "notification", packed); + return; + } + const fresh = await Notifications.findOneBy({ id: notification.id }); if (!fresh) return; // We execute this before, because the server side "read" check doesnt work well with push notifications, the app and service worker will decide themself