Files
Curse/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts
かっこかり b361a10c48 Merge commit from fork
* Tighten security in `HashtagChannel`

* Add isNoteVisibleForMe in stream channel

Co-Authored-By: Julia Johannesen <julia@insertdomain.name>

* Tighten note visibility checks in WebSocket (No.1)

* refactor

* Fix main channel

Co-Authored-By: Julia Johannesen <julia@insertdomain.name>

* fix typo

* fix missing lockdown (requireSigninToViewContents) checks

* fix(backend): streamingでのロックダウン挙動修正

* fix: 引用リノートを無条件で隠していた問題を修正

* fix: 引用リノートを単純にリノート場合に内容が見えることがある問題を修正

* refac

* fix

* fix

* fix

* Update docs

---------

Co-authored-by: Julia Johannesen <julia@insertdomain.name>
Co-authored-by: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com>
2026-03-09 08:15:31 +09:00

133 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.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 {
constructor(
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>();
// 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');
}
}
// 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 };
}
/**
* hiddenLayersに基づいてートの内容を隠す。
*
* この処理は渡された `note` を直接変更します。
*
* @param note - 処理対象のノート
* @param hiddenLayers - 隠す階層のセット
*/
@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);
}
}
/**
* ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。
*
* この処理は渡された `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 };
}
this.applyHiding(note, result.hiddenLayers);
return { shouldSkip: false };
}
}