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 da31854e9e..9f8871432f 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 @@ -76,7 +76,7 @@ CREATE TABLE note ( -- Store all posts "reactions" map, -- Reactions "noteEdit" set>, -- Edit History "updatedAt" timestamp, - PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility") + PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility", "score") ) WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE INDEX note_by_uri ON note ("uri"); @@ -91,7 +91,8 @@ CREATE MATERIALIZED VIEW note_by_id AS AND "userId" IS NOT NULL AND "userHost" IS NOT NULL AND "visibility" IS NOT NULL - PRIMARY KEY ("id", "createdAt", "createdAtDate", "userId", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY ("id", "createdAt", "createdAtDate", "userId", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW note_by_user_id AS @@ -101,7 +102,8 @@ CREATE MATERIALIZED VIEW note_by_user_id AS AND "createdAtDate" IS NOT NULL AND "userHost" IS NOT NULL AND "visibility" IS NOT NULL - PRIMARY KEY ("userId", "createdAt", "createdAtDate", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY ("userId", "createdAt", "createdAtDate", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW note_by_renote_id AS @@ -112,7 +114,8 @@ CREATE MATERIALIZED VIEW note_by_renote_id AS AND "userId" IS NOT NULL AND "userHost" IS NOT NULL AND "visibility" IS NOT NULL - PRIMARY KEY ("renoteId", "createdAt", "createdAtDate", "userId", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY ("renoteId", "createdAt", "createdAtDate", "userId", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW note_by_renote_id_and_user_id AS @@ -123,7 +126,8 @@ CREATE MATERIALIZED VIEW note_by_renote_id_and_user_id AS AND "userId" IS NOT NULL AND "userHost" IS NOT NULL AND "visibility" IS NOT NULL - PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW note_by_channel_id AS @@ -134,7 +138,8 @@ CREATE MATERIALIZED VIEW note_by_channel_id AS AND "userId" IS NOT NULL AND "userHost" IS NOT NULL AND "visibility" IS NOT NULL - PRIMARY KEY ("channelId", "createdAt", "createdAtDate", "userId", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY ("channelId", "createdAt", "createdAtDate", "userId", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW global_timeline AS @@ -143,8 +148,9 @@ CREATE MATERIALIZED VIEW global_timeline AS AND "createdAt" IS NOT NULL AND "userId" IS NOT NULL AND "userHost" IS NOT NULL + AND "score" IS NOT NULL AND "visibility" = 'public' - PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility") + PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW local_timeline AS @@ -154,7 +160,8 @@ CREATE MATERIALIZED VIEW local_timeline AS AND "userId" IS NOT NULL AND "userHost" = 'local' AND "visibility" = 'public' - PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility") + AND "score" IS NOT NULL + PRIMARY KEY ("createdAtDate", "createdAt", "userId", "userHost", "visibility", "score") WITH CLUSTERING ORDER BY ("createdAt" DESC); CREATE MATERIALIZED VIEW score_feed AS @@ -165,7 +172,8 @@ CREATE MATERIALIZED VIEW score_feed AS AND "userHost" IS NOT NULL AND "score" IS NOT NULL AND "visibility" = 'public' - PRIMARY KEY ("createdAtDate", "score", "createdAt", "userId", "userHost", "visibility") + AND "score" > 0 + PRIMARY KEY ("createdAtDate", "score", "userHost", "createdAt", "userId" "visibility") WITH CLUSTERING ORDER BY ("score" DESC, "createdAt" DESC); CREATE TABLE home_timeline ( diff --git a/packages/backend/src/db/cql.ts b/packages/backend/src/db/cql.ts index 23520f2eba..18d49698a0 100644 --- a/packages/backend/src/db/cql.ts +++ b/packages/backend/src/db/cql.ts @@ -143,6 +143,9 @@ export const scyllaQueries = { byDate: `SELECT * FROM global_timeline WHERE "createdAtDate" = ?`, }, }, + scoreFeed: { + select: `SELECT * FROM score_feed WHERE "createdAtDate" = ?`, + }, reaction: { insert: `INSERT INTO reaction ("id", "noteId", "userId", "reaction", "emoji", "createdAt") diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index c8362f21df..f2191cdd5d 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -1,7 +1,23 @@ -import { Notes } from "@/models/index.js"; +import { Notes, UserProfiles } from "@/models/index.js"; import define from "../../define.js"; import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; +import { + type ScyllaNote, + scyllaClient, + prepared, + parseScyllaNote, + filterMutedNote, + filterMutedUser, + filterBlockUser, +} from "@/db/scylla.js"; +import { + InstanceMutingsCache, + UserBlockedCache, + UserBlockingCache, + UserMutingsCache, + userWordMuteCache, +} from "@/misc/cache.js"; export const meta = { tags: ["notes"], @@ -37,15 +53,61 @@ export const paramDef = { required: [], } as const; +const ONEDAY = 1000 * 60 * 60 * 24; + export default define(meta, paramDef, async (ps, user) => { const max = 30; - const day = 1000 * 60 * 60 * 24 * ps.days; + const day = ONEDAY * ps.days; + + if (scyllaClient) { + let [mutedUserIds, mutedInstances, blockerIds, blockingIds]: string[][] = + []; + let mutedWords: string[][] = []; + + const foundNotes: ScyllaNote[] = []; + let searchedDays = 0; + let targetDay = new Date(); + + if (user) { + [mutedUserIds, mutedInstances, mutedWords, blockerIds, blockingIds] = + await Promise.all([ + UserMutingsCache.init(user.id).then((cache) => cache.getAll()), + InstanceMutingsCache.init(user.id).then((cache) => cache.getAll()), + userWordMuteCache + .fetchMaybe(user.id, () => + UserProfiles.findOne({ + select: ["mutedWords"], + where: { userId: user.id }, + }).then((profile) => profile?.mutedWords), + ) + .then((words) => words ?? []), + UserBlockedCache.init(user.id).then((cache) => cache.getAll()), + UserBlockingCache.init(user.id).then((cache) => cache.getAll()), + ]); + } + + while (foundNotes.length < max && searchedDays < ps.days) { + searchedDays++; + let notes = await scyllaClient + .execute(prepared.scoreFeed.select, [targetDay], { prepare: true }) + .then((result) => result.rows.map(parseScyllaNote)); + + if (user) { + notes = await filterMutedUser(notes, user, mutedUserIds, mutedInstances); + notes = await filterMutedNote(notes, user, mutedWords); + notes = await filterBlockUser(notes, user, [...blockerIds, ...blockingIds]); + } + + foundNotes.push(...notes); + targetDay = new Date(targetDay.getTime() - ONEDAY); + } + } const query = Notes.createQueryBuilder("note") .addSelect("note.score") .andWhere("note.score > 0") .andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) }) - .andWhere("note.visibility = 'public'") + .andWhere("note.visibility = 'public'"); switch (ps.origin) { case "local":