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): インスタンスミュートが効かない問題の修正について言及
This commit is contained in:
anatawa12 2023-12-04 14:38:21 +09:00 committed by GitHub
parent b2c4973cda
commit 18109fcef7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 176 additions and 179 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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;
}

View file

@ -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,