diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index 49f93ad108..34810059da 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -259,7 +259,7 @@ export class QueryService { @bindThis public generateVisibilityQuery(q: SelectQueryBuilder, me?: { id: MiUser['id'] } | null): void { - // This code must always be synchronized with the checks in Notes.isVisibleForMe. + // This code must always be synchronized with the checks in NoteEntityService.isVisibleForMe and Stream abstract class Channel.isNoteVisibleForMe. if (me == null) { q.andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index e7847ba74e..26830c31d0 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.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'; @@ -66,6 +67,7 @@ export class NoteEntityService implements OnModuleInit { private reactionService: ReactionService; private reactionsBufferingService: ReactionsBufferingService; private idService: IdService; + private cacheService: CacheService; private noteLoader = new DebounceLoader(this.findNoteOrFail); constructor( @@ -101,6 +103,7 @@ export class NoteEntityService implements OnModuleInit { //private reactionService: ReactionService, //private reactionsBufferingService: ReactionsBufferingService, //private idService: IdService, + //private cacheService: CacheService, ) { } @@ -111,6 +114,7 @@ export class NoteEntityService implements OnModuleInit { this.reactionService = this.moduleRef.get('ReactionService'); this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); this.idService = this.moduleRef.get('IdService'); + this.cacheService = this.moduleRef.get('CacheService'); } @bindThis @@ -125,75 +129,65 @@ 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 (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) { - hide = true; - } + const hiddenBefore = packedNote.user.makeNotesHiddenBefore; + if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) { + 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 @@ -278,7 +272,7 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise { - // This code must always be synchronized with the checks in generateVisibilityQuery. + // This code must always be synchronized with the checks in QueryService.generateVisibilityQuery. // visibility が specified かつ自分が指定されていなかったら非表示 if (note.visibility === 'specified') { if (meId == null) { @@ -468,8 +462,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/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 8259a2a9e4..e228d51103 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js'; import { ChatRoomChannel } from './api/stream/channels/chat-room.js'; import { ReversiChannel } from './api/stream/channels/reversi.js'; import { ReversiGameChannel } from './api/stream/channels/reversi-game.js'; +import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @Module({ @@ -98,6 +99,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j QueueStatsChannel, ServerStatsChannel, UserListChannel, + NoteStreamingHidingService, OpenApiServerService, OAuth2ProviderService, ], diff --git a/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts b/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts new file mode 100644 index 0000000000..459fa30fa9 --- /dev/null +++ b/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts @@ -0,0 +1,132 @@ +/* + * 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 }; + +/** 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 { + 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に基づいてノートの内容を隠す。 + * + * この処理は渡された `note` を直接変更します。 + * + * @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); + } + } + + /** + * ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。 + * + * この処理は渡された `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 }; + } +} diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 86b073414d..21036ee351 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -64,6 +64,43 @@ export default abstract class Channel { return this.connection.subscriber; } + protected isNoteVisibleForMe(note: Packed<'Note'>): boolean { + // This code must always be synchronized with the checks in QueryService.generateVisibilityQuery. + const meId = this.connection.user?.id ?? null; + + // visibility が specified かつ自分が指定されていなかったら非表示 + if (note.visibility === 'specified') { + if (meId == null) { + return false; + } else if (meId === note.userId) { + return true; + } else { + // 指定されているかどうか + return note.visibleUserIds?.some(id => meId === id) ?? false; + } + } + + // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 + if (note.visibility === 'followers') { + if (meId == null) { + return false; + } else if (meId === note.userId) { + return true; + } else if (note.reply && (meId === note.reply.userId)) { + // 自分の投稿に対するリプライ + return true; + } else if (note.mentions && note.mentions.some(id => meId === id)) { + // 自分へのメンション + return true; + } else { + // フォロワーかどうか + return Object.hasOwn(this.following, note.userId); + } + } + + return true; + } + /* * ミュートとブロックされてるを処理する */ diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index ece9d2c8b1..a37c2b8fcf 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -5,7 +5,9 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.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 ChannelRequest } from '../channel.js'; @@ -24,6 +26,7 @@ export class AntennaChannel extends Channel { request: ChannelRequest, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onEvent = this.onEvent.bind(this); @@ -43,8 +46,21 @@ export class AntennaChannel extends Channel { if (data.type === 'note') { const 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; + + if (this.user) { + 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 1706b17526..80352df2c2 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -6,6 +6,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; @@ -26,6 +27,7 @@ export class ChannelChannel extends Channel { request: ChannelRequest, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -48,12 +50,18 @@ export class ChannelChannel extends Channel { if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; + if (!this.isNoteVisibleForMe(note)) return; 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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 be6be1b1e7..2b971d93db 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; @@ -29,6 +30,7 @@ export class GlobalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -60,10 +62,15 @@ export 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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 1456b4f262..66a0df3e6b 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -7,12 +7,12 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type ChannelRequest } from '../channel.js'; import { REQUEST } from '@nestjs/core'; - @Injectable({ scope: Scope.TRANSIENT }) export class HashtagChannel extends Channel { public readonly chName = 'hashtag'; @@ -25,6 +25,7 @@ export class HashtagChannel extends Channel { request: ChannelRequest, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -33,7 +34,11 @@ export class HashtagChannel extends Channel { @bindThis public async init(params: JsonObject) { if (!Array.isArray(params.q)) return; - if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return; + if (!params.q.every((x): x is string[] => ( + Array.isArray(x) && + x.length >= 1 && + x.every(y => typeof y === 'string') + ))) return; this.q = params.q; // Subscribe stream @@ -46,12 +51,21 @@ export class HashtagChannel extends Channel { const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag)))); if (!matched) return; + if (!this.isNoteVisibleForMe(note)) return; + if (note.user.requireSigninToViewContents && this.user == null) return; + if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; + if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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 665c11b692..4742b0ed32 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -6,6 +6,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; @@ -26,6 +27,7 @@ export class HomeTimelineChannel extends Channel { request: ChannelRequest, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -55,11 +57,7 @@ export class HomeTimelineChannel extends Channel { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; } - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; - } + if (!this.isNoteVisibleForMe(note)) return; if (note.reply) { const reply = note.reply; @@ -84,10 +82,15 @@ export 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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 54250d2a90..7acd945f54 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; @@ -31,6 +32,7 @@ export class HybridTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -75,12 +77,7 @@ export class HybridTimelineChannel extends Channel { } } - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; - } - + if (!this.isNoteVisibleForMe(note)) return; if (this.isNoteMutedOrBlocked(note)) return; if (note.reply) { @@ -104,10 +101,15 @@ export 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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 b394e9663f..115b3a1415 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; @@ -30,6 +31,7 @@ export class LocalTimelineChannel extends Channel { private metaService: MetaService, private roleService: RoleService, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -70,10 +72,15 @@ export 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 2ce53ac288..68a249cbee 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -47,8 +47,8 @@ export class MainChannel extends Channel { } case 'mention': { if (isInstanceMuted(data.body, new Set(this.userProfile?.mutedInstances ?? []))) return; - - if (this.userIdsWhoMeMuting.has(data.body.userId)) return; + if (!this.isNoteVisibleForMe(data.body)) return; + if (this.isNoteMutedOrBlocked(data.body)) return; if (data.body.isHidden) { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true, 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 99e0b69023..a1be92a5c8 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,8 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.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 ChannelRequest } from '../channel.js'; @@ -25,6 +27,7 @@ export class RoleTimelineChannel extends Channel { private noteEntityService: NoteEntityService, private roleservice: RoleService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.onNote = this.onNote.bind(this); @@ -47,9 +50,24 @@ export class RoleTimelineChannel extends Channel { return; } if (note.visibility !== 'public') return; + if (note.user.requireSigninToViewContents && this.user == null) return; + if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; + 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; + + if (this.user) { + 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 2f7345e150..ae6386855b 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -7,6 +7,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteStreamingHidingService } from '../NoteStreamingHidingService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; @@ -36,6 +37,7 @@ export class UserListChannel extends Channel { request: ChannelRequest, private noteEntityService: NoteEntityService, + private noteStreamingHidingService: NoteStreamingHidingService, ) { super(request); //this.updateListUsers = this.updateListUsers.bind(this); @@ -96,11 +98,7 @@ export class UserListChannel extends Channel { if (!Object.hasOwn(this.membershipsMap, note.userId)) return; - if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; - } else if (note.visibility === 'specified') { - if (!note.visibleUserIds!.includes(this.user!.id)) return; - } + if (!this.isNoteVisibleForMe(note)) return; if (note.reply) { const reply = note.reply; @@ -117,10 +115,15 @@ export 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; + const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); + if (shouldSkip) return; + + if (this.user) { + 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; + } } }