mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-03-20 19:20:41 +00:00
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>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
### Server
|
||||
- Fix: 自分の行ったフォロワー限定投稿または指名投稿に自分自身でリアクションなどを行った場合のイベントが流れない問題を修正
|
||||
- Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正)
|
||||
- Fix: WebSocket接続におけるノートの非表示ロジックを修正
|
||||
|
||||
|
||||
## 2026.3.1
|
||||
|
||||
@@ -6,16 +6,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
|
||||
type HiddenLayer = 'note' | 'renote' | 'renoteRenote';
|
||||
|
||||
type LockdownCheckResult =
|
||||
| { shouldSkip: true }
|
||||
| { shouldSkip: false; hiddenLayers: Set<HiddenLayer> };
|
||||
|
||||
/** Streamにおいて、ノートを隠す(hideNote)を適用するためのService */
|
||||
@Injectable()
|
||||
export class NoteStreamingHidingService {
|
||||
@@ -23,110 +18,52 @@ export class NoteStreamingHidingService {
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* ノートの可視性を判定する
|
||||
*
|
||||
* @param note - 判定対象のノート
|
||||
* @param meId - 閲覧者のユーザーID(未ログインの場合はnull)
|
||||
* @returns shouldSkip: true の場合はノートを流さない、false の場合は hiddenLayers に基づいて隠す
|
||||
*/
|
||||
@bindThis
|
||||
public async shouldHide(
|
||||
note: Packed<'Note'>,
|
||||
meId: MiUser['id'] | null,
|
||||
): Promise<LockdownCheckResult> {
|
||||
const hiddenLayers = new Set<HiddenLayer>();
|
||||
private collectRenoteChain(note: Packed<'Note'>): Packed<'Note'>[] {
|
||||
const renoteChain: Packed<'Note'>[] = [];
|
||||
|
||||
// 1階層目: note自体
|
||||
const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId);
|
||||
if (shouldHideThisNote) {
|
||||
if (isRenotePacked(note) && isQuotePacked(note)) {
|
||||
// 引用リノートの場合、内容を隠して流す
|
||||
hiddenLayers.add('note');
|
||||
} else if (isRenotePacked(note)) {
|
||||
// 純粋リノートの場合、流さない
|
||||
return { shouldSkip: true };
|
||||
} else {
|
||||
// 通常ノートの場合、内容を隠して流す
|
||||
hiddenLayers.add('note');
|
||||
}
|
||||
for (let current: Packed<'Note'> | null | undefined = note; current != null; current = current.renote) {
|
||||
renoteChain.push(current);
|
||||
}
|
||||
|
||||
// 2階層目: note.renote
|
||||
if (isRenotePacked(note) && note.renote) {
|
||||
const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId);
|
||||
if (shouldHideRenote) {
|
||||
if (isQuotePacked(note)) {
|
||||
// noteが引用リノートの場合、renote部分だけ隠す
|
||||
hiddenLayers.add('renote');
|
||||
} else {
|
||||
// noteが純粋リノートの場合、流さない
|
||||
return { shouldSkip: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3階層目: note.renote.renote
|
||||
if (isRenotePacked(note) && note.renote &&
|
||||
isRenotePacked(note.renote) && note.renote.renote) {
|
||||
const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId);
|
||||
if (shouldHideRenoteRenote) {
|
||||
if (isQuotePacked(note.renote)) {
|
||||
// note.renoteが引用リノートの場合、renote.renote部分だけ隠す
|
||||
hiddenLayers.add('renoteRenote');
|
||||
} else {
|
||||
// note.renoteが純粋リノートの場合、note.renoteの意味がなくなるので流さない
|
||||
return { shouldSkip: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldSkip: false, hiddenLayers };
|
||||
return renoteChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* hiddenLayersに基づいてノートの内容を隠す。
|
||||
* ストリーミング配信用にノートの内容を隠す(あるいはそもそも送信しない)判定及び処理を行う。
|
||||
*
|
||||
* この処理は渡された `note` を直接変更します。
|
||||
* 隠す処理が必要な場合は元のノートをクローンして変更を適用したものを返し、
|
||||
* 送信すべきでない場合は `null` を返す。
|
||||
* 変更が不要な場合は元のノートの参照をそのまま返す。
|
||||
*
|
||||
* @param note - 処理対象のノート
|
||||
* @param hiddenLayers - 隠す階層のセット
|
||||
* @param meId - 閲覧者のユーザー ID (未ログインの場合は `null`)
|
||||
* @returns 配信するノートオブジェクト、または配信スキップの場合は `null`
|
||||
*/
|
||||
@bindThis
|
||||
public applyHiding(
|
||||
note: Packed<'Note'>,
|
||||
hiddenLayers: Set<HiddenLayer>,
|
||||
): void {
|
||||
if (hiddenLayers.has('note')) {
|
||||
this.noteEntityService.hideNote(note);
|
||||
}
|
||||
if (hiddenLayers.has('renote') && note.renote) {
|
||||
this.noteEntityService.hideNote(note.renote);
|
||||
}
|
||||
if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) {
|
||||
this.noteEntityService.hideNote(note.renote.renote);
|
||||
}
|
||||
}
|
||||
public async filter(note: Packed<'Note'>, meId: MiUser['id'] | null): Promise<Packed<'Note'> | null> {
|
||||
const renoteChain = this.collectRenoteChain(note);
|
||||
const shouldHide = await Promise.all(renoteChain.map(n => this.noteEntityService.shouldHideNote(n, meId)));
|
||||
|
||||
/**
|
||||
* ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。
|
||||
*
|
||||
* この処理は渡された `note` を直接変更します。
|
||||
*
|
||||
* @param note - 処理対象のノート(必要に応じて内容が隠される)
|
||||
* @param meId - 閲覧者のユーザーID(未ログインの場合はnull)
|
||||
* @returns shouldSkip: true の場合はノートを流さない
|
||||
*/
|
||||
@bindThis
|
||||
public async processHiding(
|
||||
note: Packed<'Note'>,
|
||||
meId: MiUser['id'] | null,
|
||||
): Promise<{ shouldSkip: boolean }> {
|
||||
const result = await this.shouldHide(note, meId);
|
||||
if (result.shouldSkip) {
|
||||
return { shouldSkip: true };
|
||||
if (!shouldHide.some(h => h)) {
|
||||
// 隠す必要がない場合は元のノートをそのまま返す
|
||||
return note;
|
||||
}
|
||||
this.applyHiding(note, result.hiddenLayers);
|
||||
return { shouldSkip: false };
|
||||
|
||||
if (renoteChain.some(n => isRenotePacked(n) && !isQuotePacked(n))) {
|
||||
// 純粋リノートの場合は配信をスキップする
|
||||
return null;
|
||||
}
|
||||
|
||||
const clonedNote = deepClone(note);
|
||||
let currentCloned = clonedNote;
|
||||
|
||||
for (let i = 0; i < renoteChain.length; i++) {
|
||||
if (shouldHide[i]) {
|
||||
this.noteEntityService.hideNote(currentCloned);
|
||||
}
|
||||
currentCloned = currentCloned.renote!;
|
||||
}
|
||||
|
||||
return clonedNote;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,13 +62,14 @@ export class AntennaChannel extends Channel {
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['antenna']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
let note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
if (!this.isNoteVisibleForMe(note)) return;
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -53,8 +53,10 @@ export class ChannelChannel extends Channel {
|
||||
if (!this.isNoteVisibleForMe(note)) return;
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -62,8 +62,10 @@ export class GlobalTimelineChannel extends Channel {
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -59,8 +59,10 @@ export class HashtagChannel extends Channel {
|
||||
if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return;
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -82,8 +82,10 @@ export class HomeTimelineChannel extends Channel {
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -101,8 +101,10 @@ export class HybridTimelineChannel extends Channel {
|
||||
}
|
||||
}
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -72,8 +72,10 @@ export class LocalTimelineChannel extends Channel {
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class RoleTimelineChannel extends Channel {
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
let note = data.body;
|
||||
|
||||
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
|
||||
return;
|
||||
@@ -56,8 +56,9 @@ export class RoleTimelineChannel extends Channel {
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
@@ -117,8 +117,10 @@ export class UserListChannel extends Channel {
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null);
|
||||
if (shouldSkip) return;
|
||||
const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null);
|
||||
if (!filtered) return;
|
||||
// eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全
|
||||
note = filtered;
|
||||
|
||||
if (this.user) {
|
||||
if (isRenotePacked(note) && !isQuotePacked(note)) {
|
||||
|
||||
Reference in New Issue
Block a user