From 7a494b2aa7e0337a220b8988122839e9a7b75538 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sun, 26 Nov 2023 10:02:22 +0900 Subject: fix(backend): rename FunoutTimelineService to FanoutTimelineService (#12453) --- packages/backend/src/core/NoteCreateService.ts | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) (limited to 'packages/backend/src/core/NoteCreateService.ts') diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 86f220abd0..fd87edc28e 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -54,7 +54,7 @@ import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; @@ -194,7 +194,7 @@ export class NoteCreateService implements OnApplicationShutdown { private idService: IdService, private globalEventService: GlobalEventService, private queueService: QueueService, - private funoutTimelineService: FunoutTimelineService, + private fanoutTimelineService: FanoutTimelineService, private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, @@ -843,9 +843,9 @@ export class NoteCreateService implements OnApplicationShutdown { const r = this.redisForTimelines.pipeline(); if (note.channelId) { - this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); + this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -855,9 +855,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -895,9 +895,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -913,36 +913,36 @@ export class NoteCreateService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); } } if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL - this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); } } // 自分自身以外への返信 if (note.replyId && note.replyUserId !== note.userId) { - this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); } } else { - this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { - this.funoutTimelineService.push('localTimeline', note.id, 1000, r); + this.fanoutTimelineService.push('localTimeline', note.id, 1000, r); if (note.fileIds.length > 0) { - this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); + this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } } -- cgit v1.2.3-freya From 18109fcef760dee8171364fd0382375c4047b8e7 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Mon, 4 Dec 2023 14:38:21 +0900 Subject: Filter User / Instance Mutes in FanoutTimelineEndpointService (#12565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: unnecessary logging in FanoutTimelineEndpointService * chore: TimelineOptions * chore: add FanoutTimelineName type * chore: forbid specifying both withReplies and withFiles since it's not implemented correctly * chore: filter mutes, replies, renotes, files in FanoutTimelineEndpointService * revert unintended changes * use isReply in NoteCreateService * fix: excludePureRenotes is not implemented * fix: replies to me is excluded from local timeline * chore(frontend): forbid enabling both withReplies and withFiles * docs(changelog): インスタンスミュートが効かない問題の修正について言及 --- CHANGELOG.md | 3 + .../src/core/FanoutTimelineEndpointService.ts | 103 +++++++++++++++------ packages/backend/src/core/FanoutTimelineService.ts | 36 ++++++- packages/backend/src/core/NoteCreateService.ts | 7 +- packages/backend/src/misc/is-reply.ts | 10 ++ .../src/server/api/endpoints/channels/timeline.ts | 16 +--- .../server/api/endpoints/notes/hybrid-timeline.ts | 40 +++----- .../server/api/endpoints/notes/local-timeline.ts | 40 +++----- .../src/server/api/endpoints/notes/timeline.ts | 20 +--- .../api/endpoints/notes/user-list-timeline.ts | 36 +------ .../src/server/api/endpoints/users/notes.ts | 34 +++---- packages/frontend/src/components/MkMenu.vue | 2 +- packages/frontend/src/pages/timeline.vue | 4 +- packages/frontend/src/types/menu.ts | 2 +- packages/frontend/src/ui/deck/tl-column.vue | 2 + 15 files changed, 176 insertions(+), 179 deletions(-) create mode 100644 packages/backend/src/misc/is-reply.ts (limited to 'packages/backend/src/core/NoteCreateService.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 453bdeff59..51eb5400ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,9 @@ - Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正 - Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443 - Fix: 「みつける」のなかにミュートしたユーザが現れてしまう問題を修正 #12383 +- Fix: Social/Local/Home Timelineにてインスタンスミュートが効かない問題 +- Fix: ユーザのノート一覧にてインスタンスミュートが効かない問題 +- Fix: チャンネルのノート一覧にてインスタンスミュートが効かない問題 ## 2023.11.1 diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 157fcbe877..6775f0051a 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -11,7 +11,29 @@ import type { MiNote } from '@/models/Note.js'; import { Packed } from '@/misc/json-schema.js'; import type { NotesRepository } from '@/models/_.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; +import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { CacheService } from '@/core/CacheService.js'; +import { isReply } from '@/misc/is-reply.js'; +import { isInstanceMuted } from '@/misc/is-instance-muted.js'; + +type TimelineOptions = { + untilId: string | null, + sinceId: string | null, + limit: number, + allowPartial: boolean, + me?: { id: MiUser['id'] } | undefined | null, + useDbFallback: boolean, + redisTimelines: FanoutTimelineName[], + noteFilter?: (note: MiNote) => boolean, + alwaysIncludeMyNotes?: boolean; + ignoreAuthorFromMute?: boolean; + excludeNoFiles?: boolean; + excludeReplies?: boolean; + excludePureRenotes: boolean; + dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, +}; @Injectable() export class FanoutTimelineEndpointService { @@ -20,37 +42,18 @@ export class FanoutTimelineEndpointService { private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, + private cacheService: CacheService, private fanoutTimelineService: FanoutTimelineService, ) { } @bindThis - async timeline(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - allowPartial: boolean, - me?: { id: MiUser['id'] } | undefined | null, - useDbFallback: boolean, - redisTimelines: string[], - noteFilter: (note: MiNote) => boolean, - dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, - }): Promise[]> { + async timeline(ps: TimelineOptions): Promise[]> { return await this.noteEntityService.packMany(await this.getMiNotes(ps), ps.me); } @bindThis - private async getMiNotes(ps: { - untilId: string | null, - sinceId: string | null, - limit: number, - allowPartial: boolean, - me?: { id: MiUser['id'] } | undefined | null, - useDbFallback: boolean, - redisTimelines: string[], - noteFilter: (note: MiNote) => boolean, - dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, - }): Promise { + private async getMiNotes(ps: TimelineOptions): Promise { let noteIds: string[]; let shouldFallbackToDb = false; @@ -67,10 +70,57 @@ export class FanoutTimelineEndpointService { shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); if (!shouldFallbackToDb) { + let filter = ps.noteFilter ?? (_note => true); + + if (ps.alwaysIncludeMyNotes && ps.me) { + const me = ps.me; + const parentFilter = filter; + filter = (note) => note.userId === me.id || parentFilter(note); + } + + if (ps.excludeNoFiles) { + const parentFilter = filter; + filter = (note) => note.fileIds.length !== 0 && parentFilter(note); + } + + if (ps.excludeReplies) { + const parentFilter = filter; + filter = (note) => !isReply(note, ps.me?.id) && parentFilter(note); + } + + if (ps.excludePureRenotes) { + const parentFilter = filter; + filter = (note) => !isPureRenote(note) && parentFilter(note); + } + + if (ps.me) { + const me = ps.me; + const [ + userIdsWhoMeMuting, + userIdsWhoMeMutingRenotes, + userIdsWhoBlockingMe, + userMutedInstances, + ] = 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)), + ]); + + const parentFilter = filter; + filter = (note) => { + if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromMute)) return false; + if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false; + if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false; + if (isInstanceMuted(note, userMutedInstances)) return false; + + return parentFilter(note); + }; + } + const redisTimeline: MiNote[] = []; let readFromRedis = 0; let lastSuccessfulRate = 1; // rateをキャッシュする? - let trialCount = 1; while ((redisResultIds.length - readFromRedis) !== 0) { const remainingToRead = ps.limit - redisTimeline.length; @@ -81,12 +131,10 @@ export class FanoutTimelineEndpointService { readFromRedis += noteIds.length; - const gotFromDb = await this.getAndFilterFromDb(noteIds, ps.noteFilter); + const gotFromDb = await this.getAndFilterFromDb(noteIds, filter); redisTimeline.push(...gotFromDb); lastSuccessfulRate = gotFromDb.length / noteIds.length; - console.log(`fanoutTimelineTrial#${trialCount++}: req: ${ps.limit}, tried: ${noteIds.length}, got: ${gotFromDb.length}, rate: ${lastSuccessfulRate}, total: ${redisTimeline.length}, fromRedis: ${redisResultIds.length}`); - if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) { // 十分Redisからとれた return redisTimeline.slice(0, ps.limit); @@ -97,7 +145,6 @@ export class FanoutTimelineEndpointService { const remainingToRead = ps.limit - redisTimeline.length; const gotFromDb = await ps.dbFallback(noteIds[noteIds.length - 1], ps.sinceId, remainingToRead); redisTimeline.push(...gotFromDb); - console.log(`fanoutTimelineTrial#db: req: ${ps.limit}, tried: ${remainingToRead}, got: ${gotFromDb.length}, since: ${noteIds[noteIds.length - 1]}, until: ${ps.untilId}, total: ${redisTimeline.length}`); return redisTimeline; } diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index 6a1b0aa879..654a035a5f 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -9,6 +9,34 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +export type FanoutTimelineName = + // home timeline + | `homeTimeline:${string}` + | `homeTimelineWithFiles:${string}` // only notes with files are included + // local timeline + | `localTimeline` // replies are not included + | `localTimelineWithFiles` // only non-reply notes with files are included + | `localTimelineWithReplies` // only replies are included + + // antenna + | `antennaTimeline:${string}` + + // user timeline + | `userTimeline:${string}` // replies are not included + | `userTimelineWithFiles:${string}` // only non-reply notes with files are included + | `userTimelineWithReplies:${string}` // only replies are included + | `userTimelineWithChannel:${string}` // only channel notes are included, replies are included + + // user list timelines + | `userListTimeline:${string}` + | `userListTimelineWithFiles:${string}` // only notes with files are included + + // channel timelines + | `channelTimeline:${string}` // replies are included + + // role timelines + | `roleTimeline:${string}` // any notes are included + @Injectable() export class FanoutTimelineService { constructor( @@ -20,7 +48,7 @@ export class FanoutTimelineService { } @bindThis - public push(tl: string, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { + public push(tl: FanoutTimelineName, id: string, maxlen: number, pipeline: Redis.ChainableCommander) { // リモートから遅れて届いた(もしくは後から追加された)投稿日時が古い投稿が追加されるとページネーション時に問題を引き起こすため、 // 3分以内に投稿されたものでない場合、Redisにある最古のIDより新しい場合のみ追加する if (this.idService.parse(id).date.getTime() > Date.now() - 1000 * 60 * 3) { @@ -41,7 +69,7 @@ export class FanoutTimelineService { } @bindThis - public get(name: string, untilId?: string | null, sinceId?: string | null) { + public get(name: FanoutTimelineName, untilId?: string | null, sinceId?: string | null) { if (untilId && sinceId) { return this.redisForTimelines.lrange('list:' + name, 0, -1) .then(ids => ids.filter(id => id < untilId && id > sinceId).sort((a, b) => a > b ? -1 : 1)); @@ -58,7 +86,7 @@ export class FanoutTimelineService { } @bindThis - public getMulti(name: string[], untilId?: string | null, sinceId?: string | null): Promise { + public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise { const pipeline = this.redisForTimelines.pipeline(); for (const n of name) { pipeline.lrange('list:' + n, 0, -1); @@ -79,7 +107,7 @@ export class FanoutTimelineService { } @bindThis - public purge(name: string) { + public purge(name: FanoutTimelineName) { return this.redisForTimelines.del('list:' + name); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index fd87edc28e..0110ebaf5e 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -57,6 +57,7 @@ import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { isReply } from '@/misc/is-reply.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -891,7 +892,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue; // 「自分自身への返信 or そのフォロワーへの返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === following.followerId)) { + if (isReply(note, following.followerId)) { if (!following.withReplies) continue; } @@ -909,7 +910,7 @@ export class NoteCreateService implements OnApplicationShutdown { ) continue; // 「自分自身への返信 or そのリストの作成者への返信」のどちらでもない場合 - if (note.replyId && !(note.replyUserId === note.userId || note.replyUserId === userListMembership.userListUserId)) { + if (isReply(note, userListMembership.userListUserId)) { if (!userListMembership.withReplies) continue; } @@ -927,7 +928,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // 自分自身以外への返信 - if (note.replyId && note.replyUserId !== note.userId) { + if (isReply(note)) { this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { diff --git a/packages/backend/src/misc/is-reply.ts b/packages/backend/src/misc/is-reply.ts new file mode 100644 index 0000000000..964c2aa153 --- /dev/null +++ b/packages/backend/src/misc/is-reply.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MiUser } from '@/models/User.js'; + +export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean { + return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId; +} diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 9ef494d6d8..006228ceee 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -4,15 +4,13 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, MiNote, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; @@ -94,12 +92,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - return await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -108,11 +100,7 @@ export default class extends Endpoint { // eslint- me, useDbFallback: true, redisTimelines: [`channelTimeline:${channel.id}`], - noteFilter: note => { - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - - return true; - }, + excludePureRenotes: false, dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); }, diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index deb9e014c4..effcbaf2ee 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -12,9 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { CacheService } from '@/core/CacheService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -43,6 +42,12 @@ export const meta = { code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dfaa3eb7-8002-4cb7-bcc4-1095df46656f' + }, }, } as const; @@ -93,6 +98,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.stlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { @@ -114,17 +121,7 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]); - - let timelineConfig: string[]; + let timelineConfig: FanoutTimelineName[]; if (ps.withFiles) { timelineConfig = [ @@ -152,21 +149,8 @@ export default class extends Endpoint { // eslint- me, redisTimelines: timelineConfig, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - noteFilter: (note) => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 97b05016ec..e8ba39bbf0 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiNote, NotesRepository } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -13,7 +13,6 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; @@ -39,6 +38,12 @@ export const meta = { code: 'LTL_DISABLED', id: '45a6eb02-7695-4393-b023-dd3be9aaaefd', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793' + }, }, } as const; @@ -82,6 +87,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.ltlDisabled); } + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { @@ -102,16 +109,6 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - ]) : [new Set(), new Set(), new Set()]; - const timeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -120,22 +117,9 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'], - noteFilter: note => { - if (me && (note.userId === me.id)) { - return true; - } - if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false; - if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (me && isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - - return true; - }, + alwaysIncludeMyNotes: true, + excludeReplies: !ps.withReplies, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 74d0a6e0c0..790bcbe151 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -13,7 +13,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; @@ -98,14 +97,8 @@ export default class extends Endpoint { // eslint- const [ followings, - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, ] = await Promise.all([ this.cacheService.userFollowingsCache.fetch(me.id), - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), ]); const timeline = this.fanoutTimelineEndpointService.timeline({ @@ -116,18 +109,9 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, noteFilter: note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } if (note.reply && note.reply.visibility === 'followers') { if (!Object.hasOwn(followings, note.reply.userId)) return false; } diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index f8f64738fe..10d3a7a697 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,20 +5,17 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -84,7 +81,6 @@ export default class extends Endpoint { // eslint- private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, private idService: IdService, - private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, private metaService: MetaService, @@ -121,18 +117,6 @@ export default class extends Endpoint { // eslint- await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - userIdsWhoMeMutingRenotes, - userIdsWhoBlockingMe, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.renoteMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), - ]); - const timeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -141,22 +125,8 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], - noteFilter: note => { - if (note.userId === me.id) { - return true; - } - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false; - if (ps.withRenotes === false) return false; - } - } - if (isInstanceMuted(note, userMutedInstances)) return false; - - return true; - }, + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, { untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 4a358b39cb..b32128a8aa 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -11,11 +11,12 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; import { QueryService } from '@/core/QueryService.js'; import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['users', 'notes'], @@ -36,6 +37,12 @@ export const meta = { code: 'NO_SUCH_USER', id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b', }, + + bothWithRepliesAndWithFiles: { + message: 'Specifying both withReplies and withFiles is not supported', + code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', + id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', + }, }, } as const; @@ -77,6 +84,8 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); + if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + if (!serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, @@ -91,13 +100,7 @@ export default class extends Endpoint { // eslint- return await this.noteEntityService.packMany(timeline, me); } - const [ - userIdsWhoMeMuting, - ] = me ? await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - ]) : [new Set()]; - - const redisTimelines = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; + const redisTimelines: FanoutTimelineName[] = [ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`]; if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`); if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`); @@ -112,18 +115,11 @@ export default class extends Endpoint { // eslint- me, redisTimelines, useDbFallback: true, + ignoreAuthorFromMute: true, + excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies + excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files + excludePureRenotes: !ps.withRenotes, noteFilter: note => { - if (ps.withFiles && note.fileIds.length === 0) { - return false; - } - if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false; - - if (note.renoteId) { - if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) { - if (ps.withRenotes === false) return false; - } - } - if (note.channel?.isSensitive && !isSelf) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 9457bf385f..4fafd35f72 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -202,7 +202,7 @@ function focusDown() { } function switchItem(item: MenuSwitch & { ref: any }) { - if (item.disabled) return; + if (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value) return; item.ref = !item.ref; } diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index cfe270aefb..b390d1931d 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -157,17 +157,17 @@ const headerActions = $computed(() => { os.popupMenu([{ type: 'switch', text: i18n.ts.showRenotes, - icon: 'ti ti-repeat', ref: $$(withRenotes), }, src === 'local' || src === 'social' ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: $$(withReplies), + disabled: $$(onlyFiles), } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, - icon: 'ti ti-photo', ref: $$(onlyFiles), + disabled: src === 'local' || src === 'social' ? $$(withReplies) : false, }], ev.currentTarget ?? ev.target); }, }, diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index 66061fcd70..fbe627176b 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -14,7 +14,7 @@ export type MenuLabel = { type: 'label', text: string }; export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean | Ref }; export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 9f24ea31ed..41582bbfe3 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -120,10 +120,12 @@ const menu = [{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: $$(withReplies), + disabled: $$(onlyFiles), } : undefined, { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: $$(onlyFiles), + disabled: props.column.tl === 'local' || props.column.tl === 'social' ? $$(withReplies) : false, }]; -- cgit v1.2.3-freya From bb38e62ae696873990d0252c66200c361878e9f8 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Mon, 4 Dec 2023 17:56:48 +0900 Subject: chore: 自分へのリプライのみ走査するように (#12570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/FanoutTimelineService.ts | 1 + packages/backend/src/core/NoteCreateService.ts | 3 +++ .../backend/src/server/api/endpoints/notes/local-timeline.ts | 9 ++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/core/NoteCreateService.ts') diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index 654a035a5f..9b2678fbcd 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -17,6 +17,7 @@ export type FanoutTimelineName = | `localTimeline` // replies are not included | `localTimelineWithFiles` // only non-reply notes with files are included | `localTimelineWithReplies` // only replies are included + | `localTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. // antenna | `antennaTimeline:${string}` diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 0110ebaf5e..45dfbb87aa 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -933,6 +933,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); + if (note.replyUserHost == null) { + this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); + } } } else { this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index e8ba39bbf0..3fd4dc83fb 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -42,7 +42,7 @@ export const meta = { bothWithRepliesAndWithFiles: { message: 'Specifying both withReplies and withFiles is not supported', code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', - id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793' + id: 'dd9c8400-1cb5-4eef-8a31-200c5f933793', }, }, } as const; @@ -116,9 +116,12 @@ export default class extends Endpoint { // eslint- allowPartial: ps.allowPartial, me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ['localTimeline', 'localTimelineWithReplies'], + redisTimelines: + ps.withFiles ? ['localTimelineWithFiles'] + : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] + : me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] + : ['localTimeline'], alwaysIncludeMyNotes: true, - excludeReplies: !ps.withReplies, excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, -- cgit v1.2.3-freya From f6ff3b1f1a7047a29337575c08849be99ac15a4b Mon Sep 17 00:00:00 2001 From: 1STEP621 <86859447+1STEP621@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:49:19 +0900 Subject: Fix: Renoteの判定が間違っているのを修正 (#12706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * RNと引用RNの判定が間違っているのを修正 * remove dump.rdb * update CHANGELOG.md * lint fix --- CHANGELOG.md | 2 ++ packages/backend/src/core/NoteCreateService.ts | 11 ++++++++--- packages/frontend/src/components/MkCwButton.vue | 2 ++ packages/frontend/src/components/MkNote.vue | 3 ++- 4 files changed, 14 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/core/NoteCreateService.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6df0a167..6a99a6ca2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ - Fix: ノート中の絵文字をタップして「リアクションする」からリアクションした際にリアクションサウンドが鳴らない不具合を修正 - Fix: ノート中のリアクションの表示を微調整 #12650 - Fix: AiScriptの`readline`が不正な値を返すことがある問題を修正 +- Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正 +- Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。 ### Server - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 45dfbb87aa..9fe965b139 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check blocking - if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) { + if (data.renote && this.isQuote(data)) { if (data.renote.userHost === null) { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); @@ -622,7 +622,7 @@ export class NoteCreateService implements OnApplicationShutdown { // If it is renote if (data.renote) { - const type = data.text ? 'quote' : 'renote'; + const type = this.isQuote(data) ? 'quote' : 'renote'; // Notify if (data.renote.userHost === null) { @@ -729,6 +729,11 @@ export class NoteCreateService implements OnApplicationShutdown { return false; } + @bindThis + private isQuote(note: Option): boolean { + return !!note.text || !!note.cw || !!note.files || !!note.poll; + } + @bindThis private incRenoteCount(renote: MiNote) { this.notesRepository.createQueryBuilder().update() @@ -794,7 +799,7 @@ export class NoteCreateService implements OnApplicationShutdown { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; - const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0) + const content = data.renote && this.isQuote(data) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 70b7bc8295..a25193a57c 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -17,6 +17,7 @@ import MkButton from '@/components/MkButton.vue'; const props = defineProps<{ modelValue: boolean; text: string | null; + renote: Misskey.entities.Note | null; files: Misskey.entities.DriveFile[]; poll?: { expiresAt: string | null; @@ -41,6 +42,7 @@ const emit = defineEmits<{ const label = computed(() => { return concat([ props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [], + props.renote != null ? [i18n.ts.quote] : [], props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [], props.poll != null ? [i18n.ts.poll] : [], ] as string[][]).join(' / '); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e723198a17..609b381598 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

@@ -229,6 +229,7 @@ if (noteViewInterruptors.length > 0) { const isRenote = ( note.value.renote != null && note.value.text == null && + note.value.cw == null && note.value.fileIds.length === 0 && note.value.poll == null ); -- cgit v1.2.3-freya From 52b94dbc4ab60aa6efba927ef8df509a3bb0d046 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 22 Dec 2023 14:03:39 +0900 Subject: fix: 引用RNがpure RNとして連合され、pure RNが引用RNとして連合される (#12744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: quote notes are rendered as pure renote * fix: filesが指定されてて空配列のときにQuote扱いされる * chore: isQuoteの仕様をmisc/is-quote.tsと揃える * docs: is-quote.tsの方にNoteCreateService.isQuoteのことを書いて更新忘れを防ぐ --- packages/backend/src/core/NoteCreateService.ts | 9 +++++---- packages/backend/src/misc/is-quote.ts | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/core/NoteCreateService.ts') diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 9fe965b139..54493612b8 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check blocking - if (data.renote && this.isQuote(data)) { + if (this.isQuote(data)) { if (data.renote.userHost === null) { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); @@ -730,8 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - private isQuote(note: Option): boolean { - return !!note.text || !!note.cw || !!note.files || !!note.poll; + private isQuote(note: Option): note is Option & { renote: MiNote } { + // sync with misc/is-quote.ts + return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll); } @bindThis @@ -799,7 +800,7 @@ export class NoteCreateService implements OnApplicationShutdown { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { if (data.localOnly) return null; - const content = data.renote && this.isQuote(data) + const content = data.renote && !this.isQuote(data) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts index 059f6a4b5f..db72d1d57a 100644 --- a/packages/backend/src/misc/is-quote.ts +++ b/packages/backend/src/misc/is-quote.ts @@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js'; // eslint-disable-next-line import/no-default-export export default function(note: MiNote): boolean { + // sync with NoteCreateService.isQuote return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } -- cgit v1.2.3-freya