summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authoranatawa12 <anatawa12@icloud.com>2023-12-04 14:38:21 +0900
committerGitHub <noreply@github.com>2023-12-04 14:38:21 +0900
commit18109fcef760dee8171364fd0382375c4047b8e7 (patch)
tree20bc2c25c8e90070ea8aa77c06cb3b7fa3a28810 /packages/backend/src
parentfix dev build (#12566) (diff)
downloadmisskey-18109fcef760dee8171364fd0382375c4047b8e7.tar.gz
misskey-18109fcef760dee8171364fd0382375c4047b8e7.tar.bz2
misskey-18109fcef760dee8171364fd0382375c4047b8e7.zip
Filter User / Instance Mutes in FanoutTimelineEndpointService (#12565)
* 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): インスタンスミュートが効かない問題の修正について言及
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts103
-rw-r--r--packages/backend/src/core/FanoutTimelineService.ts36
-rw-r--r--packages/backend/src/core/NoteCreateService.ts7
-rw-r--r--packages/backend/src/misc/is-reply.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts40
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts40
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts34
10 files changed, 167 insertions, 175 deletions
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<MiNote[]>,
+};
@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<MiNote[]>,
- }): Promise<Packed<'Note'>[]> {
+ async timeline(ps: TimelineOptions): Promise<Packed<'Note'>[]> {
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<MiNote[]>,
- }): Promise<MiNote[]> {
+ private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> {
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<string[][]> {
+ public getMulti(name: FanoutTimelineName[], untilId?: string | null, sinceId?: string | null): Promise<string[][]> {
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<typeof meta, typeof paramDef> { // 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<string>()];
-
return await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@@ -108,11 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<string>(), new Set<string>(), new Set<string>()];
-
const timeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@@ -120,22 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // eslint-
return await this.noteEntityService.packMany(timeline, me);
}
- const [
- userIdsWhoMeMuting,
- ] = me ? await Promise.all([
- this.cacheService.userMutingsCache.fetch(me.id),
- ]) : [new Set<string>()];
-
- 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<typeof meta, typeof paramDef> { // 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;