From 06e944e66616d085a810b6a40690428bb2761c31 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 11 Aug 2025 16:23:41 -0400 Subject: [PATCH] check for silenced users, muted notes/threads, and note visibility in FanoutTimelineEndpointService.getMiNotes --- .../src/core/FanoutTimelineEndpointService.ts | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 40e2af1487..9bfc372e56 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -15,7 +15,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { isInstanceMuted } from '@/misc/is-instance-muted.js'; @@ -37,7 +37,9 @@ type TimelineOptions = { excludeReplies?: boolean; excludeBots?: boolean; excludePureRenotes: boolean; + includeMutedNotes?: boolean; ignoreAuthorFromUserSuspension?: boolean; + ignoreAuthorFromUserSilence?: boolean; dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; @@ -109,17 +111,24 @@ export class FanoutTimelineEndpointService { } if (ps.me) { - const me = ps.me; const [ userIdsWhoMeMuting, userIdsWhoMeMutingRenotes, userIdsWhoBlockingMe, userMutedInstances, + myFollowings, + myThreadMutings, + myNoteMutings, + me, ] = await Promise.all([ this.cacheService.userMutingsCache.fetch(ps.me.id), this.cacheService.renoteMutingsCache.fetch(ps.me.id), this.cacheService.userBlockedCache.fetch(ps.me.id), - this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), + this.cacheService.userProfileCache.fetch(ps.me.id).then(p => new Set(p.mutedInstances)), + this.cacheService.userFollowingsCache.fetch(ps.me.id).then(fs => new Set(fs.keys())), + this.cacheService.threadMutingsCache.fetch(ps.me.id), + this.cacheService.noteMutingsCache.fetch(ps.me.id), + this.cacheService.findUserById(ps.me.id), ]); const parentFilter = filter; @@ -129,6 +138,25 @@ export class FanoutTimelineEndpointService { if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false; if (isInstanceMuted(note, userMutedInstances)) return false; + // Silenced users (when logged in) + if (!ps.ignoreAuthorFromUserSilence && !myFollowings.has(note.userId)) { + if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false; + if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false; + if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false; + } + + // Muted threads / posts + if (!ps.includeMutedNotes) { + if (myThreadMutings.has(note.threadId ?? note.id) || myNoteMutings.has(note.id)) return false; + if (note.replyId && myNoteMutings.has(note.replyId)) return false; + if (note.renote && (myThreadMutings.has(note.renote.threadId ?? note.renote.id) || myNoteMutings.has(note.renote.id))) return false; + } + + // Invisible notes + if (!this.noteEntityService.isVisibleForMeSync(note, me, myFollowings, userIdsWhoBlockingMe)) { + return false; + } + return parentFilter(note); }; } @@ -154,7 +182,7 @@ export class FanoutTimelineEndpointService { replyUser: MiUser | null; }; if (!ps.ignoreAuthorFromUserSuspension) { - if (note.user!.isSuspended) return false; + if (note.user?.isSuspended) return false; } if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false; if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false; @@ -163,6 +191,20 @@ export class FanoutTimelineEndpointService { }; } + { + const parentFilter = filter; + filter = (note) => { + // Silenced users (when logged out) + if (!ps.ignoreAuthorFromUserSilence && !ps.me) { + if (note.user?.isSilenced || note.user?.instance?.isSilenced) return false; + if (note.reply?.user?.isSilenced || note.reply?.user?.instance?.isSilenced) return false; + if (note.renote?.user?.isSilenced || note.renote?.user?.instance?.isSilenced) return false; + } + + return parentFilter(note); + }; + } + const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? @@ -215,7 +257,13 @@ export class FanoutTimelineEndpointService { .leftJoinAndSelect('note.channel', 'channel') .leftJoinAndSelect('note.userInstance', 'userInstance') .leftJoinAndSelect('note.replyUserInstance', 'replyUserInstance') - .leftJoinAndSelect('note.renoteUserInstance', 'renoteUserInstance'); + .leftJoinAndSelect('note.renoteUserInstance', 'renoteUserInstance') + + // These are used to ensure full data for boosted replies. + // Without loading these relations, certain filters may fail open. + .leftJoinAndSelect('renote.reply', 'renoteReply') + .leftJoinAndSelect('renote.reply.user', 'renoteReplyUser') + .leftJoinAndSelect('renote.replyUserInstance', 'renoteReplyUserInstance'); const notes = (await query.getMany()).filter(noteFilter);