From 861cc01b0a863bb647fefbc5d7d6cab2efc646eb Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:52:11 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix(backend):=20streaming=E3=81=A7=E3=81=AE?= =?UTF-8?q?=E3=83=AD=E3=83=83=E3=82=AF=E3=83=80=E3=82=A6=E3=83=B3=E6=8C=99?= =?UTF-8?q?=E5=8B=95=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/entities/NoteEntityService.ts | 108 ++++++++---------- .../src/server/api/stream/channels/antenna.ts | 27 +++++ .../src/server/api/stream/channels/channel.ts | 27 ++++- .../api/stream/channels/global-timeline.ts | 27 ++++- .../src/server/api/stream/channels/hashtag.ts | 27 ++++- .../api/stream/channels/home-timeline.ts | 29 ++++- .../api/stream/channels/hybrid-timeline.ts | 27 ++++- .../api/stream/channels/local-timeline.ts | 27 ++++- .../api/stream/channels/role-timeline.ts | 27 +++++ .../server/api/stream/channels/user-list.ts | 27 ++++- 10 files changed, 266 insertions(+), 87 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 6871ba2c72..3008ec96d1 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { CacheService } from '@/core/CacheService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; @@ -100,6 +101,7 @@ export class NoteEntityService implements OnModuleInit { //private reactionService: ReactionService, //private reactionsBufferingService: ReactionsBufferingService, //private idService: IdService, + private cacheService: CacheService, ) { } @@ -129,80 +131,70 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { - if (meId === packedNote.userId) return; - + public async shouldHideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { + if (meId === packedNote.userId) return false; // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) - let hide = false; if (packedNote.user.requireSigninToViewContents && meId == null) { - hide = true; + return true; } - if (!hide) { - const hiddenBefore = packedNote.user.makeNotesHiddenBefore; - if ((hiddenBefore != null) - && ( - (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000))) - || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000)) - ) - ) { - hide = true; - } + const hiddenBefore = packedNote.user.makeNotesHiddenBefore; + if ((hiddenBefore != null) + && ( + (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000))) + || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000)) + ) + ) { + return true; } // visibility が specified かつ自分が指定されていなかったら非表示 - if (!hide) { - if (packedNote.visibility === 'specified') { - if (meId == null) { - hide = true; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some(id => meId === id); + if (packedNote.visibility === 'specified') { + if (meId == null) { + return true; + } else { + // 指定されているかどうか + const specified = packedNote.visibleUserIds!.some(id => meId === id); - if (!specified) { - hide = true; - } + if (!specified) { + return true; } } } // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (!hide) { - if (packedNote.visibility === 'followers') { - if (meId == null) { - hide = true; - } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.userId, - followerId: meId, - }, - }); - - hide = !isFollowing; + if (packedNote.visibility === 'followers') { + if (meId == null) { + return true; + } else if (packedNote.reply && (meId === packedNote.reply.userId)) { + // 自分の投稿に対するリプライ + return false; + } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { + // 自分へのメンション + return false; + } else { + // フォロワーかどうか + const followings = await this.cacheService.userFollowingsCache.fetch(meId); + if (!Object.hasOwn(followings, packedNote.userId)) { + return true; } } } - if (hide) { - packedNote.visibleUserIds = undefined; - packedNote.fileIds = []; - packedNote.files = []; - packedNote.text = null; - packedNote.poll = undefined; - packedNote.cw = null; - packedNote.isHidden = true; - // TODO: hiddenReason みたいなのを提供しても良さそう - } + return false; + } + + @bindThis + public hideNote(packedNote: Packed<'Note'>): void { + packedNote.visibleUserIds = undefined; + packedNote.fileIds = []; + packedNote.files = []; + packedNote.text = null; + packedNote.poll = undefined; + packedNote.cw = null; + packedNote.isHidden = true; + // TODO: hiddenReason みたいなのを提供しても良さそう } @bindThis @@ -477,8 +469,8 @@ export class NoteEntityService implements OnModuleInit { this.treatVisibility(packed); - if (!opts.skipHide) { - await this.hideNote(packed, meId); + if (!opts.skipHide && await this.shouldHideNote(packed, meId)) { + this.hideNote(packed); } return packed; diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index e08562fdf9..538fe6f1a1 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -43,6 +44,32 @@ class AntennaChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない(そもそもここにたどり着くことは無いとは思うけど) + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } + } + } + this.send('note', note); } else { this.send(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index c07eaac98d..4574280879 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -47,10 +47,29 @@ class ChannelChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index d7c781ad12..af320795be 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -58,10 +58,29 @@ class GlobalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index c911d63642..3419c66ab8 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -46,10 +46,29 @@ class HashtagChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index eb5b4a8c6c..06adeafd88 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import { isRenotePacked, isQuotePacked, isRenote } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -82,10 +82,29 @@ class HomeTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 2155e02012..4aa68c1805 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -102,10 +102,29 @@ class HybridTimelineChannel extends Channel { } } - if (this.user && note.renoteId && !note.text) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 3d7ed6acdb..7f40cc489e 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -68,10 +68,29 @@ class LocalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index fcfa26c38b..2cf59ffe7e 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -48,6 +49,32 @@ class RoleTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } + } + } + this.send('note', note); } else { this.send(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 5bfd8fa68c..c1297990a9 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -111,10 +111,29 @@ class UserListChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { - if (note.renote && Object.keys(note.renote.reactions).length > 0) { - const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); - note.renote.myReaction = myRenoteReaction; + if (this.user) { + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); + if (shouldHideThisNote) { + this.noteEntityService.hideNote(note); + } + + if (isRenotePacked(note) && note.renote) { + const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); + + if (isQuotePacked(note)) { + // 引用リノートの場合、リノート部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else if (shouldHideRenote) { + // 純粋なリノートの場合、流さない + return; + } + } + + if (isRenotePacked(note) && !isQuotePacked(note)) { + if (note.renote && Object.keys(note.renote.reactions).length > 0) { + const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); + note.renote.myReaction = myRenoteReaction; + } } } From cbb5f99024378f223ab40c25ff34dcb1a03d77c1 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:56:27 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E5=BC=95=E7=94=A8=E3=83=AA=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=82=92=E7=84=A1=E6=9D=A1=E4=BB=B6=E3=81=A7?= =?UTF-8?q?=E9=9A=A0=E3=81=97=E3=81=A6=E3=81=84=E3=81=9F=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/stream/channels/antenna.ts | 2 +- packages/backend/src/server/api/stream/channels/channel.ts | 2 +- .../backend/src/server/api/stream/channels/global-timeline.ts | 2 +- packages/backend/src/server/api/stream/channels/hashtag.ts | 2 +- .../backend/src/server/api/stream/channels/home-timeline.ts | 2 +- .../backend/src/server/api/stream/channels/hybrid-timeline.ts | 2 +- .../backend/src/server/api/stream/channels/local-timeline.ts | 2 +- .../backend/src/server/api/stream/channels/role-timeline.ts | 2 +- packages/backend/src/server/api/stream/channels/user-list.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 538fe6f1a1..184b1144b0 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -53,7 +53,7 @@ class AntennaChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 4574280879..4fe681f79f 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -56,7 +56,7 @@ class ChannelChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index af320795be..10cec0cdcd 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -67,7 +67,7 @@ class GlobalTimelineChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 3419c66ab8..f12342519f 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -55,7 +55,7 @@ class HashtagChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 06adeafd88..cebb63b415 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -91,7 +91,7 @@ class HomeTimelineChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 4aa68c1805..7563049984 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -111,7 +111,7 @@ class HybridTimelineChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 7f40cc489e..5b198ef933 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -77,7 +77,7 @@ class LocalTimelineChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 2cf59ffe7e..cd9766ff9a 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -58,7 +58,7 @@ class RoleTimelineChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index c1297990a9..33e1af4ab3 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -120,7 +120,7 @@ class UserListChannel extends Channel { if (isRenotePacked(note) && note.renote) { const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - if (isQuotePacked(note)) { + if (shouldHideRenote && isQuotePacked(note)) { // 引用リノートの場合、リノート部分だけ隠す this.noteEntityService.hideNote(note.renote); } else if (shouldHideRenote) { From a24b2f8e8a0682678d0262ecb73ae64d32ed971e Mon Sep 17 00:00:00 2001 From: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:39:11 +0900 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E5=BC=95=E7=94=A8=E3=83=AA=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=82=92=E5=8D=98=E7=B4=94=E3=81=AB=E3=83=AA?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E5=A0=B4=E5=90=88=E3=81=AB=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E3=81=8C=E8=A6=8B=E3=81=88=E3=82=8B=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=81=8C=E3=81=82=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/ServerModule.ts | 2 + .../api/stream/NoteStreamingFilterService.ts | 78 +++++++++++++++++++ .../src/server/api/stream/channels/antenna.ts | 24 ++---- .../src/server/api/stream/channels/channel.ts | 24 ++---- .../api/stream/channels/global-timeline.ts | 24 ++---- .../src/server/api/stream/channels/hashtag.ts | 24 ++---- .../api/stream/channels/home-timeline.ts | 26 ++----- .../api/stream/channels/hybrid-timeline.ts | 24 ++---- .../api/stream/channels/local-timeline.ts | 24 ++---- .../api/stream/channels/role-timeline.ts | 24 ++---- .../server/api/stream/channels/user-list.ts | 24 ++---- 11 files changed, 144 insertions(+), 154 deletions(-) create mode 100644 packages/backend/src/server/api/stream/NoteStreamingFilterService.ts diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 111421472d..9afbdc4e5a 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -49,6 +49,7 @@ import { ChatUserChannelService } from './api/stream/channels/chat-user.js'; import { ChatRoomChannelService } from './api/stream/channels/chat-room.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; +import { NoteStreamingFilterService } from './api/stream/NoteStreamingFilterService.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @Module({ @@ -98,6 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j QueueStatsChannelService, ServerStatsChannelService, UserListChannelService, + NoteStreamingFilterService, OpenApiServerService, OAuth2ProviderService, ], diff --git a/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts b/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts new file mode 100644 index 0000000000..7c01f74f18 --- /dev/null +++ b/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts @@ -0,0 +1,78 @@ +/* + * 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'; + +@Injectable() +export class NoteStreamingFilterService { + constructor( + private noteEntityService: NoteEntityService, + ) {} + + /** + * ストリーミング配信用にノートの可視性をフィルタリングする + * ロックダウン設定やvisibility設定に基づいて、ノートを隠すか流さないかを判定する + * + * @param note - フィルタリング対象のノート + * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) + * @returns 'show'(そのまま/隠して流す)または 'skip'(流さない) + */ + @bindThis + public async filterForStreaming( + note: Packed<'Note'>, + meId: MiUser['id'] | null, + ): Promise<'show' | 'skip'> { + // 1階層目: note自体 + const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId); + if (shouldHideThisNote) { + if (isRenotePacked(note) && isQuotePacked(note)) { + // 引用リノートの場合、内容を隠して流す + this.noteEntityService.hideNote(note); + } else if (isRenotePacked(note)) { + // 純粋リノートの場合、流さない + return 'skip'; + } else { + // 通常ノートの場合、内容を隠して流す + this.noteEntityService.hideNote(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部分だけ隠す + this.noteEntityService.hideNote(note.renote); + } else { + // noteが純粋リノートの場合、流さない + return 'skip'; + } + } + } + + // 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部分だけ隠す + this.noteEntityService.hideNote(note.renote.renote); + } else { + // note.renoteが純粋リノートの場合、note.renoteの意味がなくなるので流さない + return 'skip'; + } + } + } + + return 'show'; + } +} diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 184b1144b0..3f0b839f1f 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -10,6 +10,7 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class AntennaChannel extends Channel { public readonly chName = 'antenna'; @@ -20,6 +21,7 @@ class AntennaChannel extends Channel { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -44,24 +46,10 @@ class AntennaChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない(そもそもここにたどり着くことは無いとは思うけど) - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -91,6 +79,7 @@ export class AntennaChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -98,6 +87,7 @@ export class AntennaChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): AntennaChannel { return new AntennaChannel( this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 4fe681f79f..05e711ed75 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -12,6 +12,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class ChannelChannel extends Channel { public readonly chName = 'channel'; @@ -21,6 +22,7 @@ class ChannelChannel extends Channel { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], ) { @@ -47,24 +49,10 @@ class ChannelChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -120,6 +108,7 @@ export class ChannelChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -127,6 +116,7 @@ export class ChannelChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): ChannelChannel { return new ChannelChannel( this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 10cec0cdcd..82fb028df0 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -12,6 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; @@ -24,6 +25,7 @@ class GlobalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -58,24 +60,10 @@ class GlobalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -104,6 +92,7 @@ export class GlobalTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -113,6 +102,7 @@ export class GlobalTimelineChannelService implements MiChannelService { this.metaService, this.roleService, this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index f12342519f..0d75c7cbee 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -11,6 +11,7 @@ import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class HashtagChannel extends Channel { public readonly chName = 'hashtag'; @@ -20,6 +21,7 @@ class HashtagChannel extends Channel { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -46,24 +48,10 @@ class HashtagChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -90,6 +78,7 @@ export class HashtagChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -97,6 +86,7 @@ export class HashtagChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): HashtagChannel { return new HashtagChannel( this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index cebb63b415..b18267dfa0 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -7,9 +7,10 @@ import { Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { isRenotePacked, isQuotePacked, isRenote } from '@/misc/is-renote.js'; +import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; @@ -21,6 +22,7 @@ class HomeTimelineChannel extends Channel { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -82,24 +84,10 @@ class HomeTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -126,6 +114,7 @@ export class HomeTimelineChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -133,6 +122,7 @@ export class HomeTimelineChannelService implements MiChannelService { public create(id: string, connection: Channel['connection']): HomeTimelineChannel { return new HomeTimelineChannel( this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 7563049984..cf5d4b5d6f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -12,6 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; @@ -26,6 +27,7 @@ class HybridTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -102,24 +104,10 @@ class HybridTimelineChannel extends Channel { } } + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -148,6 +136,7 @@ export class HybridTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -157,6 +146,7 @@ export class HybridTimelineChannelService implements MiChannelService { this.metaService, this.roleService, this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 5b198ef933..8a25195a3e 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -12,6 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; @@ -25,6 +26,7 @@ class LocalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -68,24 +70,10 @@ class LocalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -114,6 +102,7 @@ export class LocalTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -123,6 +112,7 @@ export class LocalTimelineChannelService implements MiChannelService { this.metaService, this.roleService, this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index cd9766ff9a..ccad62dd32 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -11,6 +11,7 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; @@ -21,6 +22,7 @@ class RoleTimelineChannel extends Channel { constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -49,24 +51,10 @@ class RoleTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -97,6 +85,7 @@ export class RoleTimelineChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -105,6 +94,7 @@ export class RoleTimelineChannelService implements MiChannelService { return new RoleTimelineChannel( this.noteEntityService, this.roleservice, + this.noteStreamingFilterService, id, connection, ); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 33e1af4ab3..59444455c8 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; class UserListChannel extends Channel { public readonly chName = 'userList'; @@ -27,6 +28,7 @@ class UserListChannel extends Channel { private userListsRepository: UserListsRepository, private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, id: string, connection: Channel['connection'], @@ -111,24 +113,10 @@ class UserListChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; + const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); + if (filterResult === 'skip') return; + if (this.user) { - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, this.user.id); - if (shouldHideThisNote) { - this.noteEntityService.hideNote(note); - } - - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, this.user.id); - - if (shouldHideRenote && isQuotePacked(note)) { - // 引用リノートの場合、リノート部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else if (shouldHideRenote) { - // 純粋なリノートの場合、流さない - return; - } - } - if (isRenotePacked(note) && !isQuotePacked(note)) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id); @@ -164,6 +152,7 @@ export class UserListChannelService implements MiChannelService { private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, + private noteStreamingFilterService: NoteStreamingFilterService, ) { } @@ -173,6 +162,7 @@ export class UserListChannelService implements MiChannelService { this.userListsRepository, this.userListMembershipsRepository, this.noteEntityService, + this.noteStreamingFilterService, id, connection, ); From ad1d107cd4f8bc387e38745dc39fec7d4218ff5c Mon Sep 17 00:00:00 2001 From: KanariKanaru <93921745+kanarikanaru@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:53:33 +0900 Subject: [PATCH 4/9] refac --- packages/backend/src/server/ServerModule.ts | 4 +- .../api/stream/NoteStreamingFilterService.ts | 78 ----------- .../stream/NoteStreamingLockdownService.ts | 129 ++++++++++++++++++ .../src/server/api/stream/channels/antenna.ts | 10 +- .../src/server/api/stream/channels/channel.ts | 10 +- .../api/stream/channels/global-timeline.ts | 10 +- .../src/server/api/stream/channels/hashtag.ts | 10 +- .../api/stream/channels/home-timeline.ts | 10 +- .../api/stream/channels/hybrid-timeline.ts | 10 +- .../api/stream/channels/local-timeline.ts | 10 +- .../api/stream/channels/role-timeline.ts | 10 +- .../server/api/stream/channels/user-list.ts | 10 +- 12 files changed, 176 insertions(+), 125 deletions(-) delete mode 100644 packages/backend/src/server/api/stream/NoteStreamingFilterService.ts create mode 100644 packages/backend/src/server/api/stream/NoteStreamingLockdownService.ts diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 9afbdc4e5a..04e7e84d2c 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -49,7 +49,7 @@ import { ChatUserChannelService } from './api/stream/channels/chat-user.js'; import { ChatRoomChannelService } from './api/stream/channels/chat-room.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; -import { NoteStreamingFilterService } from './api/stream/NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from './api/stream/NoteStreamingLockdownService.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @Module({ @@ -99,7 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j QueueStatsChannelService, ServerStatsChannelService, UserListChannelService, - NoteStreamingFilterService, + NoteStreamingLockdownService, OpenApiServerService, OAuth2ProviderService, ], diff --git a/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts b/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts deleted file mode 100644 index 7c01f74f18..0000000000 --- a/packages/backend/src/server/api/stream/NoteStreamingFilterService.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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'; - -@Injectable() -export class NoteStreamingFilterService { - constructor( - private noteEntityService: NoteEntityService, - ) {} - - /** - * ストリーミング配信用にノートの可視性をフィルタリングする - * ロックダウン設定やvisibility設定に基づいて、ノートを隠すか流さないかを判定する - * - * @param note - フィルタリング対象のノート - * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) - * @returns 'show'(そのまま/隠して流す)または 'skip'(流さない) - */ - @bindThis - public async filterForStreaming( - note: Packed<'Note'>, - meId: MiUser['id'] | null, - ): Promise<'show' | 'skip'> { - // 1階層目: note自体 - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId); - if (shouldHideThisNote) { - if (isRenotePacked(note) && isQuotePacked(note)) { - // 引用リノートの場合、内容を隠して流す - this.noteEntityService.hideNote(note); - } else if (isRenotePacked(note)) { - // 純粋リノートの場合、流さない - return 'skip'; - } else { - // 通常ノートの場合、内容を隠して流す - this.noteEntityService.hideNote(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部分だけ隠す - this.noteEntityService.hideNote(note.renote); - } else { - // noteが純粋リノートの場合、流さない - return 'skip'; - } - } - } - - // 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部分だけ隠す - this.noteEntityService.hideNote(note.renote.renote); - } else { - // note.renoteが純粋リノートの場合、note.renoteの意味がなくなるので流さない - return 'skip'; - } - } - } - - return 'show'; - } -} diff --git a/packages/backend/src/server/api/stream/NoteStreamingLockdownService.ts b/packages/backend/src/server/api/stream/NoteStreamingLockdownService.ts new file mode 100644 index 0000000000..e29fe3b85c --- /dev/null +++ b/packages/backend/src/server/api/stream/NoteStreamingLockdownService.ts @@ -0,0 +1,129 @@ +/* + * 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 }; + +@Injectable() +export class NoteStreamingLockdownService { + constructor( + private noteEntityService: NoteEntityService, + ) {} + + /** + * ロックダウン設定に基づいてノートの可視性を判定する(純粋関数) + * 副作用なしで判定のみを行う + * + * @param note - 判定対象のノート + * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) + * @returns shouldSkip: true の場合はノートを流さない、false の場合は hiddenLayers に基づいて隠す + */ + @bindThis + public async checkLockdown( + note: Packed<'Note'>, + meId: MiUser['id'] | null, + ): Promise { + const hiddenLayers = new Set(); + + // 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に基づいてノートの内容を隠す(副作用あり) + * + * @param note - 処理対象のノート + * @param hiddenLayers - 隠す階層のセット + */ + @bindThis + public applyHiding( + note: Packed<'Note'>, + hiddenLayers: Set, + ): 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); + } + } + + /** + * ストリーミング配信用にノートのロックダウン処理を適用する(便利メソッド) + * checkLockdown + applyHiding を一括で行う + * + * @param note - 処理対象のノート(必要に応じて内容が隠される) + * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) + * @returns shouldSkip: true の場合はノートを流さない + */ + @bindThis + public async processLockdown( + note: Packed<'Note'>, + meId: MiUser['id'] | null, + ): Promise<{ shouldSkip: boolean }> { + const result = await this.checkLockdown(note, meId); + if (result.shouldSkip) { + return { shouldSkip: true }; + } + this.applyHiding(note, result.hiddenLayers); + return { shouldSkip: false }; + } +} diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 3f0b839f1f..a4e936bfe9 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -10,7 +10,7 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class AntennaChannel extends Channel { public readonly chName = 'antenna'; @@ -21,7 +21,7 @@ class AntennaChannel extends Channel { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -46,8 +46,8 @@ class AntennaChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -79,7 +79,7 @@ export class AntennaChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 05e711ed75..d818a74025 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -12,7 +12,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class ChannelChannel extends Channel { public readonly chName = 'channel'; @@ -22,7 +22,7 @@ class ChannelChannel extends Channel { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], ) { @@ -49,8 +49,8 @@ class ChannelChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -108,7 +108,7 @@ export class ChannelChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 82fb028df0..e4d52fb84f 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -12,7 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; @@ -25,7 +25,7 @@ class GlobalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -60,8 +60,8 @@ class GlobalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -92,7 +92,7 @@ export class GlobalTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 0d75c7cbee..bb56768fad 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -11,7 +11,7 @@ import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class HashtagChannel extends Channel { public readonly chName = 'hashtag'; @@ -21,7 +21,7 @@ class HashtagChannel extends Channel { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -48,8 +48,8 @@ class HashtagChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -78,7 +78,7 @@ export class HashtagChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index b18267dfa0..cd068149a6 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; @@ -22,7 +22,7 @@ class HomeTimelineChannel extends Channel { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -84,8 +84,8 @@ class HomeTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -114,7 +114,7 @@ export class HomeTimelineChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index cf5d4b5d6f..6c038bc964 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -12,7 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class HybridTimelineChannel extends Channel { public readonly chName = 'hybridTimeline'; @@ -27,7 +27,7 @@ class HybridTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -104,8 +104,8 @@ class HybridTimelineChannel extends Channel { } } - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -136,7 +136,7 @@ export class HybridTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 8a25195a3e..631fa05c7b 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -12,7 +12,7 @@ import { RoleService } from '@/core/RoleService.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class LocalTimelineChannel extends Channel { public readonly chName = 'localTimeline'; @@ -26,7 +26,7 @@ class LocalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -70,8 +70,8 @@ class LocalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -102,7 +102,7 @@ export class LocalTimelineChannelService implements MiChannelService { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index ccad62dd32..51666f6d40 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -11,7 +11,7 @@ import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; @@ -22,7 +22,7 @@ class RoleTimelineChannel extends Channel { constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -51,8 +51,8 @@ class RoleTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -85,7 +85,7 @@ export class RoleTimelineChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, private roleservice: RoleService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 59444455c8..09fe4a657e 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -12,7 +12,7 @@ import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; -import { NoteStreamingFilterService } from '../NoteStreamingFilterService.js'; +import { NoteStreamingLockdownService } from '../NoteStreamingLockdownService.js'; class UserListChannel extends Channel { public readonly chName = 'userList'; @@ -28,7 +28,7 @@ class UserListChannel extends Channel { private userListsRepository: UserListsRepository, private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, id: string, connection: Channel['connection'], @@ -113,8 +113,8 @@ class UserListChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const filterResult = await this.noteStreamingFilterService.filterForStreaming(note, this.user?.id ?? null); - if (filterResult === 'skip') return; + const { shouldSkip: shouldSkipByLockdown } = await this.noteStreamingFilterService.processLockdown(note, this.user?.id ?? null); + if (shouldSkipByLockdown) return; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { @@ -152,7 +152,7 @@ export class UserListChannelService implements MiChannelService { private userListMembershipsRepository: UserListMembershipsRepository, private noteEntityService: NoteEntityService, - private noteStreamingFilterService: NoteStreamingFilterService, + private noteStreamingFilterService: NoteStreamingLockdownService, ) { } From dc87ed883f91474f1018b10db4e10301e3b7cb47 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 4 Dec 2025 00:11:22 +0900 Subject: [PATCH 5/9] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f7933824..dff9cb0bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Enhance: メモリ使用量を削減しました - Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上 - Enhance: 依存関係の更新 +- Fix: リアルタイム更新時にロックダウン設定が考慮されていない問題を修正 ## 2025.11.1 From b3c38d051e76ea42e20189782a99e315353c9f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:13:22 +0900 Subject: [PATCH 6/9] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea363d7d3..4cf32224f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Server - Fix: ジョブキューでSentryが有効にならない問題を修正 +- Fix: リアルタイム更新時にロックダウン設定が考慮されていない問題を修正 ## 2025.12.0 @@ -25,7 +26,6 @@ - Enhance: メモリ使用量を削減しました - Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上 - Enhance: 依存関係の更新 -- Fix: リアルタイム更新時にロックダウン設定が考慮されていない問題を修正 - Fix: セキュリティに関する修正 ## 2025.11.1 From 04aa3aee29d42050e0602ce30cc8ba6d340650c1 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:27:52 +0900 Subject: [PATCH 7/9] Update Changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c58376b7..81faf3a7b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - ### Server -- +- Fix: リアルタイム更新時にロックダウン設定が考慮されていない問題を修正 ## 2025.12.2 @@ -50,7 +50,6 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Server - Fix: ジョブキューでSentryが有効にならない問題を修正 -- Fix: リアルタイム更新時にロックダウン設定が考慮されていない問題を修正 ## 2025.12.0 From 4463533452ccb6be0e756cc74f21acef9677333e Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:39:46 +0900 Subject: [PATCH 8/9] run pnpm dedupe --- pnpm-lock.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f396918e9..0b137b7366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11018,9 +11018,6 @@ packages: vue-component-type-helpers@3.1.8: resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==} - vue-component-type-helpers@3.1.6: - resolution: {integrity: sha512-lqPXjac98uz7zh8fxHxJPJDP4SKeaYn2Fx4kQLCBthMXm9VsmPdzUWOGWMcXN7reJ9IkcBiOFxsFS/Po7dARiw==} - vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -22930,8 +22927,6 @@ snapshots: vue-component-type-helpers@3.1.8: {} - vue-component-type-helpers@3.1.6: {} - vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)): dependencies: vue: 3.5.25(typescript@5.9.3) From 563fb67b3f8b1dbf918ab255f7e88cea6a3d769f Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:47:20 +0900 Subject: [PATCH 9/9] fix --- packages/backend/src/server/ServerModule.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 8259a2a9e4..cfdf9fb5cb 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -22,6 +22,7 @@ import { SigninApiService } from './api/SigninApiService.js'; import { SigninService } from './api/SigninService.js'; import { SignupApiService } from './api/SignupApiService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; +import { NoteStreamingLockdownService } from './api/stream/NoteStreamingLockdownService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; import { HtmlTemplateService } from './web/HtmlTemplateService.js'; @@ -80,6 +81,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j SigninService, SignupApiService, StreamingApiServerService, + NoteStreamingLockdownService, MainChannel, AdminChannel, AntennaChannel,