Skip to content

Commit 2904b5a

Browse files
fix(backend): fix streaming note hiding logic (#17248)
* fix(backend): fix streaming note hiding logic * Update Changelog * refactor: avoid using generator function --------- Co-authored-by: Acid Chicken <root@acid-chicken.com>
1 parent f16ef2e commit 2904b5a

11 files changed

Lines changed: 70 additions & 116 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Server
1010
- Fix: 自分の行ったフォロワー限定投稿または指名投稿に自分自身でリアクションなどを行った場合のイベントが流れない問題を修正
1111
- Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正)
12+
- Fix: WebSocket接続におけるノートの非表示ロジックを修正
1213

1314

1415
## 2026.3.1

packages/backend/src/server/api/stream/NoteStreamingHidingService.ts

Lines changed: 33 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -6,127 +6,64 @@
66
import { Injectable } from '@nestjs/common';
77
import { bindThis } from '@/decorators.js';
88
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
9+
import { deepClone } from '@/misc/clone.js';
910
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
1011
import type { Packed } from '@/misc/json-schema.js';
1112
import type { MiUser } from '@/models/User.js';
1213

13-
type HiddenLayer = 'note' | 'renote' | 'renoteRenote';
14-
15-
type LockdownCheckResult =
16-
| { shouldSkip: true }
17-
| { shouldSkip: false; hiddenLayers: Set<HiddenLayer> };
18-
1914
/** Streamにおいて、ノートを隠す(hideNote)を適用するためのService */
2015
@Injectable()
2116
export class NoteStreamingHidingService {
2217
constructor(
2318
private noteEntityService: NoteEntityService,
2419
) {}
2520

26-
/**
27-
* ノートの可視性を判定する
28-
*
29-
* @param note - 判定対象のノート
30-
* @param meId - 閲覧者のユーザーID(未ログインの場合はnull)
31-
* @returns shouldSkip: true の場合はノートを流さない、false の場合は hiddenLayers に基づいて隠す
32-
*/
33-
@bindThis
34-
public async shouldHide(
35-
note: Packed<'Note'>,
36-
meId: MiUser['id'] | null,
37-
): Promise<LockdownCheckResult> {
38-
const hiddenLayers = new Set<HiddenLayer>();
21+
private collectRenoteChain(note: Packed<'Note'>): Packed<'Note'>[] {
22+
const renoteChain: Packed<'Note'>[] = [];
3923

40-
// 1階層目: note自体
41-
const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId);
42-
if (shouldHideThisNote) {
43-
if (isRenotePacked(note) && isQuotePacked(note)) {
44-
// 引用リノートの場合、内容を隠して流す
45-
hiddenLayers.add('note');
46-
} else if (isRenotePacked(note)) {
47-
// 純粋リノートの場合、流さない
48-
return { shouldSkip: true };
49-
} else {
50-
// 通常ノートの場合、内容を隠して流す
51-
hiddenLayers.add('note');
52-
}
53-
}
54-
55-
// 2階層目: note.renote
56-
if (isRenotePacked(note) && note.renote) {
57-
const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId);
58-
if (shouldHideRenote) {
59-
if (isQuotePacked(note)) {
60-
// noteが引用リノートの場合、renote部分だけ隠す
61-
hiddenLayers.add('renote');
62-
} else {
63-
// noteが純粋リノートの場合、流さない
64-
return { shouldSkip: true };
65-
}
66-
}
67-
}
68-
69-
// 3階層目: note.renote.renote
70-
if (isRenotePacked(note) && note.renote &&
71-
isRenotePacked(note.renote) && note.renote.renote) {
72-
const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId);
73-
if (shouldHideRenoteRenote) {
74-
if (isQuotePacked(note.renote)) {
75-
// note.renoteが引用リノートの場合、renote.renote部分だけ隠す
76-
hiddenLayers.add('renoteRenote');
77-
} else {
78-
// note.renoteが純粋リノートの場合、note.renoteの意味がなくなるので流さない
79-
return { shouldSkip: true };
80-
}
81-
}
24+
for (let current: Packed<'Note'> | null | undefined = note; current != null; current = current.renote) {
25+
renoteChain.push(current);
8226
}
8327

84-
return { shouldSkip: false, hiddenLayers };
28+
return renoteChain;
8529
}
8630

8731
/**
88-
* hiddenLayersに基づいてノートの内容を隠す
32+
* ストリーミング配信用にノートの内容を隠す(あるいはそもそも送信しない)判定及び処理を行う
8933
*
90-
* この処理は渡された `note` を直接変更します。
34+
* 隠す処理が必要な場合は元のノートをクローンして変更を適用したものを返し、
35+
* 送信すべきでない場合は `null` を返す。
36+
* 変更が不要な場合は元のノートの参照をそのまま返す。
9137
*
9238
* @param note - 処理対象のノート
93-
* @param hiddenLayers - 隠す階層のセット
39+
* @param meId - 閲覧者のユーザー ID (未ログインの場合は `null`)
40+
* @returns 配信するノートオブジェクト、または配信スキップの場合は `null`
9441
*/
9542
@bindThis
96-
public applyHiding(
97-
note: Packed<'Note'>,
98-
hiddenLayers: Set<HiddenLayer>,
99-
): void {
100-
if (hiddenLayers.has('note')) {
101-
this.noteEntityService.hideNote(note);
102-
}
103-
if (hiddenLayers.has('renote') && note.renote) {
104-
this.noteEntityService.hideNote(note.renote);
43+
public async filter(note: Packed<'Note'>, meId: MiUser['id'] | null): Promise<Packed<'Note'> | null> {
44+
const renoteChain = this.collectRenoteChain(note);
45+
const shouldHide = await Promise.all(renoteChain.map(n => this.noteEntityService.shouldHideNote(n, meId)));
46+
47+
if (!shouldHide.some(h => h)) {
48+
// 隠す必要がない場合は元のノートをそのまま返す
49+
return note;
10550
}
106-
if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) {
107-
this.noteEntityService.hideNote(note.renote.renote);
51+
52+
if (renoteChain.some(n => isRenotePacked(n) && !isQuotePacked(n))) {
53+
// 純粋リノートの場合は配信をスキップする
54+
return null;
10855
}
109-
}
11056

111-
/**
112-
* ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。
113-
*
114-
* この処理は渡された `note` を直接変更します。
115-
*
116-
* @param note - 処理対象のノート(必要に応じて内容が隠される)
117-
* @param meId - 閲覧者のユーザーID(未ログインの場合はnull)
118-
* @returns shouldSkip: true の場合はノートを流さない
119-
*/
120-
@bindThis
121-
public async processHiding(
122-
note: Packed<'Note'>,
123-
meId: MiUser['id'] | null,
124-
): Promise<{ shouldSkip: boolean }> {
125-
const result = await this.shouldHide(note, meId);
126-
if (result.shouldSkip) {
127-
return { shouldSkip: true };
57+
const clonedNote = deepClone(note);
58+
let currentCloned = clonedNote;
59+
60+
for (let i = 0; i < renoteChain.length; i++) {
61+
if (shouldHide[i]) {
62+
this.noteEntityService.hideNote(currentCloned);
63+
}
64+
currentCloned = currentCloned.renote!;
12865
}
129-
this.applyHiding(note, result.hiddenLayers);
130-
return { shouldSkip: false };
66+
67+
return clonedNote;
13168
}
13269
}

packages/backend/src/server/api/stream/channels/antenna.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,14 @@ export class AntennaChannel extends Channel {
6262
@bindThis
6363
private async onEvent(data: GlobalEvents['antenna']['payload']) {
6464
if (data.type === 'note') {
65-
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
65+
let note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
6666

6767
if (!this.isNoteVisibleForMe(note)) return;
6868
if (this.isNoteMutedOrBlocked(note)) return;
6969

70-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
71-
if (shouldSkip) return;
70+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
71+
if (!filtered) return;
72+
note = filtered;
7273

7374
if (this.user) {
7475
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/channel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ export class ChannelChannel extends Channel {
5353
if (!this.isNoteVisibleForMe(note)) return;
5454
if (this.isNoteMutedOrBlocked(note)) return;
5555

56-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
57-
if (shouldSkip) return;
56+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
57+
if (!filtered) return;
58+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
59+
note = filtered;
5860

5961
if (this.user) {
6062
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/global-timeline.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export class GlobalTimelineChannel extends Channel {
6262

6363
if (this.isNoteMutedOrBlocked(note)) return;
6464

65-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
66-
if (shouldSkip) return;
65+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
66+
if (!filtered) return;
67+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
68+
note = filtered;
6769

6870
if (this.user) {
6971
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/hashtag.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ export class HashtagChannel extends Channel {
5959
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
6060
if (this.isNoteMutedOrBlocked(note)) return;
6161

62-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
63-
if (shouldSkip) return;
62+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
63+
if (!filtered) return;
64+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
65+
note = filtered;
6466

6567
if (this.user) {
6668
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/home-timeline.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ export class HomeTimelineChannel extends Channel {
8282

8383
if (this.isNoteMutedOrBlocked(note)) return;
8484

85-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
86-
if (shouldSkip) return;
85+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
86+
if (!filtered) return;
87+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
88+
note = filtered;
8789

8890
if (this.user) {
8991
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/hybrid-timeline.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ export class HybridTimelineChannel extends Channel {
101101
}
102102
}
103103

104-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
105-
if (shouldSkip) return;
104+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
105+
if (!filtered) return;
106+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
107+
note = filtered;
106108

107109
if (this.user) {
108110
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/local-timeline.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export class LocalTimelineChannel extends Channel {
7272

7373
if (this.isNoteMutedOrBlocked(note)) return;
7474

75-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
76-
if (shouldSkip) return;
75+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
76+
if (!filtered) return;
77+
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
78+
note = filtered;
7779

7880
if (this.user) {
7981
if (isRenotePacked(note) && !isQuotePacked(note)) {

packages/backend/src/server/api/stream/channels/role-timeline.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class RoleTimelineChannel extends Channel {
4444
@bindThis
4545
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
4646
if (data.type === 'note') {
47-
const note = data.body;
47+
let note = data.body;
4848

4949
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
5050
return;
@@ -56,8 +56,9 @@ export class RoleTimelineChannel extends Channel {
5656

5757
if (this.isNoteMutedOrBlocked(note)) return;
5858

59-
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
60-
if (shouldSkip) return;
59+
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
60+
if (!filtered) return;
61+
note = filtered;
6162

6263
if (this.user) {
6364
if (isRenotePacked(note) && !isQuotePacked(note)) {

0 commit comments

Comments
 (0)