server: handle note visibility in SQL

This allows to check visibility recursively, which should hopefully
solve problems with timelines not showing up properly.

Changelog: Changed
This commit is contained in:
Johann150 2022-09-02 17:35:02 +02:00
parent 73d546372e
commit 8f5952bb7d
Signed by: Johann150
GPG key ID: 9EE6577A2A06F8F1
3 changed files with 56 additions and 33 deletions

View file

@ -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');
}
}

View file

@ -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<boolean> {
// 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) {

View file

@ -3,40 +3,10 @@ import { User } from '@/models/entities/user.js';
import { Followings } from '@/models/index.js';
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, 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 });
}
}