diff --git a/packages/backend/migration/1662132062000-note-visibility-function.js b/packages/backend/migration/1662132062000-note-visibility-function.js new file mode 100644 index 000000000..6dd88f912 --- /dev/null +++ b/packages/backend/migration/1662132062000-note-visibility-function.js @@ -0,0 +1,53 @@ +export class noteVisibilityFunction1662132062000 { + name = 'noteVisibilityFunction1662132062000'; + + async up(queryRunner) { + await queryRunner.query(` + CREATE OR REPLACE FUNCTION note_visible(note_id varchar, user_id varchar) RETURNS BOOLEAN + LANGUAGE SQL + STABLE + CALLED ON NULL INPUT + AS $$ + SELECT CASE + WHEN note_id IS NULL THEN TRUE + WHEN NOT EXISTS (SELECT 1 FROM note WHERE id = note_id) THEN FALSE + WHEN user_id IS NULL THEN ( + -- simplified check without logged in user + SELECT + visibility IN ('public', 'home') + -- check reply / renote recursively + AND note_visible("replyId", NULL) + AND note_visible("renoteId", NULL) + FROM note WHERE note.id = note_id + ) ELSE ( + SELECT + ( + visibility IN ('public', 'home') + OR + user_id = "userId" + OR + user_id = ANY("visibleUserIds") + OR + user_id = ANY("mentions") + OR ( + visibility = 'followers' + AND + EXISTS ( + SELECT 1 FROM following WHERE "followeeId" = "userId" AND "followerId" = user_id + ) + ) + ) + -- check reply / renote recursively + AND note_visible("replyId", user_id) + AND note_visible("renoteId", user_id) + FROM note WHERE note.id = note_id + ) + END; + $$; + `); + } + + async down(queryRunner) { + await queryRunner.query('DROP FUNCTION note_visible'); + } +} diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 03874d358..1ff4194ec 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -77,7 +77,7 @@ async function populateMyReaction(note: Note, meId: User['id'], _hint_?: { export const NoteRepository = db.getRepository(Note).extend({ async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { - // This code must always be synchronized with the checks in generateVisibilityQuery. + // This code must always be synchronized with the `note_visible` SQL function. // visibility が specified かつ自分が指定されていなかったら非表示 if (note.visibility === 'specified') { if (meId == null) { diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts index 12b610cde..26df89b2c 100644 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ b/packages/backend/src/server/api/common/generate-visibility-query.ts @@ -3,40 +3,10 @@ import { User } from '@/models/entities/user.js'; import { Followings } from '@/models/index.js'; export function generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: User['id'] } | null) { - // This code must always be synchronized with the checks in Notes.isVisibleForMe. if (me == null) { - q.andWhere(new Brackets(qb => { qb - .where("note.visibility = 'public'") - .orWhere("note.visibility = 'home'"); - })); + q.andWhere('note_visible(note.id, null)'); } else { - const followingQuery = Followings.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :meId'); - - q.andWhere(new Brackets(qb => { qb - // 公開投稿である - .where(new Brackets(qb => { qb - .where("note.visibility = 'public'") - .orWhere("note.visibility = 'home'"); - })) - // または 自分自身 - .orWhere('note.userId = :meId') - // または 自分宛て - .orWhere(':meId = ANY(note.visibleUserIds)') - .orWhere(':meId = ANY(note.mentions)') - .orWhere(new Brackets(qb => { qb - // または フォロワー宛ての投稿であり、 - .where("note.visibility = 'followers'") - .andWhere(new Brackets(qb => { qb - // 自分がフォロワーである - .where(`note.userId IN (${ followingQuery.getQuery() })`) - // または 自分の投稿へのリプライ - .orWhere('note.replyUserId = :meId'); - })); - })); - })); - + q.andWhere('note_visible(note.id, :meId)'); q.setParameters({ meId: me.id }); } }