enhance(backend): improve fanout tl for stl
This commit is contained in:
		| @@ -5,7 +5,6 @@ | ||||
|  | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import * as Redis from 'ioredis'; | ||||
| import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import ActiveUsersChart from '@/core/chart/charts/active-users.js'; | ||||
| @@ -16,6 +15,8 @@ import { IdService } from '@/core/IdService.js'; | ||||
| import { isUserRelated } from '@/misc/is-user-related.js'; | ||||
| import { CacheService } from '@/core/CacheService.js'; | ||||
| import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; | ||||
| import { QueryService } from '@/core/QueryService.js'; | ||||
| import { UserFollowingService } from '@/core/UserFollowingService.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
|  | ||||
| export const meta = { | ||||
| @@ -63,9 +64,6 @@ export const paramDef = { | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export | ||||
| 	constructor( | ||||
| 		@Inject(DI.redisForTimelines) | ||||
| 		private redisForTimelines: Redis.Redis, | ||||
|  | ||||
| 		@Inject(DI.notesRepository) | ||||
| 		private notesRepository: NotesRepository, | ||||
|  | ||||
| @@ -75,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 		private idService: IdService, | ||||
| 		private cacheService: CacheService, | ||||
| 		private funoutTimelineService: FunoutTimelineService, | ||||
| 		private queryService: QueryService, | ||||
| 		private userFollowingService: UserFollowingService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); | ||||
| @@ -96,6 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			]); | ||||
|  | ||||
| 			let noteIds: string[]; | ||||
| 			let shouldFallbackToDb = false; | ||||
|  | ||||
| 			if (ps.withFiles) { | ||||
| 				const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([ | ||||
| @@ -116,51 +117,118 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 					'localTimeline', | ||||
| 				], untilId, sinceId); | ||||
| 				noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); | ||||
| 				shouldFallbackToDb = htlNoteIds.length === 0; | ||||
| 			} | ||||
|  | ||||
| 			noteIds.sort((a, b) => a > b ? -1 : 1); | ||||
| 			noteIds = noteIds.slice(0, ps.limit); | ||||
|  | ||||
| 			if (noteIds.length === 0) { | ||||
| 				return []; | ||||
| 			} | ||||
| 			if (!shouldFallbackToDb) { | ||||
| 				const query = this.notesRepository.createQueryBuilder('note') | ||||
| 					.where('note.id IN (:...noteIds)', { noteIds: noteIds }) | ||||
| 					.innerJoinAndSelect('note.user', 'user') | ||||
| 					.leftJoinAndSelect('note.reply', 'reply') | ||||
| 					.leftJoinAndSelect('note.renote', 'renote') | ||||
| 					.leftJoinAndSelect('reply.user', 'replyUser') | ||||
| 					.leftJoinAndSelect('renote.user', 'renoteUser') | ||||
| 					.leftJoinAndSelect('note.channel', 'channel'); | ||||
|  | ||||
| 			const query = this.notesRepository.createQueryBuilder('note') | ||||
| 				.where('note.id IN (:...noteIds)', { noteIds: noteIds }) | ||||
| 				.innerJoinAndSelect('note.user', 'user') | ||||
| 				.leftJoinAndSelect('note.reply', 'reply') | ||||
| 				.leftJoinAndSelect('note.renote', 'renote') | ||||
| 				.leftJoinAndSelect('reply.user', 'replyUser') | ||||
| 				.leftJoinAndSelect('renote.user', 'renoteUser') | ||||
| 				.leftJoinAndSelect('note.channel', 'channel'); | ||||
| 				let timeline = await query.getMany(); | ||||
|  | ||||
| 			let timeline = await query.getMany(); | ||||
|  | ||||
| 			timeline = timeline.filter(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; | ||||
| 				timeline = timeline.filter(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; | ||||
| 				}); | ||||
|  | ||||
| 				// TODO: フィルタした結果件数が足りなかった場合の対応 | ||||
|  | ||||
| 				timeline.sort((a, b) => a.id > b.id ? -1 : 1); | ||||
|  | ||||
| 				process.nextTick(() => { | ||||
| 					this.activeUsersChart.read(me); | ||||
| 				}); | ||||
|  | ||||
| 				return await this.noteEntityService.packMany(timeline, me); | ||||
| 			} else { // fallback to db | ||||
| 				const followees = await this.userFollowingService.getFollowees(me.id); | ||||
|  | ||||
| 				const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), | ||||
| 					ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) | ||||
| 					.andWhere(new Brackets(qb => { | ||||
| 						if (followees.length > 0) { | ||||
| 							const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)]; | ||||
| 							qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); | ||||
| 							qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); | ||||
| 						} else { | ||||
| 							qb.where('note.userId = :meId', { meId: me.id }); | ||||
| 							qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); | ||||
| 						} | ||||
| 					})) | ||||
| 					.innerJoinAndSelect('note.user', 'user') | ||||
| 					.leftJoinAndSelect('note.reply', 'reply') | ||||
| 					.leftJoinAndSelect('note.renote', 'renote') | ||||
| 					.leftJoinAndSelect('reply.user', 'replyUser') | ||||
| 					.leftJoinAndSelect('renote.user', 'renoteUser'); | ||||
|  | ||||
| 				this.queryService.generateVisibilityQuery(query, me); | ||||
| 				this.queryService.generateMutedUserQuery(query, me); | ||||
| 				this.queryService.generateBlockedUserQuery(query, me); | ||||
| 				this.queryService.generateMutedUserRenotesQueryForNotes(query, me); | ||||
|  | ||||
| 				if (ps.includeMyRenotes === false) { | ||||
| 					query.andWhere(new Brackets(qb => { | ||||
| 						qb.orWhere('note.userId != :meId', { meId: me.id }); | ||||
| 						qb.orWhere('note.renoteId IS NULL'); | ||||
| 						qb.orWhere('note.text IS NOT NULL'); | ||||
| 						qb.orWhere('note.fileIds != \'{}\''); | ||||
| 						qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); | ||||
| 					})); | ||||
| 				} | ||||
|  | ||||
| 				return true; | ||||
| 			}); | ||||
| 				if (ps.includeRenotedMyNotes === false) { | ||||
| 					query.andWhere(new Brackets(qb => { | ||||
| 						qb.orWhere('note.renoteUserId != :meId', { meId: me.id }); | ||||
| 						qb.orWhere('note.renoteId IS NULL'); | ||||
| 						qb.orWhere('note.text IS NOT NULL'); | ||||
| 						qb.orWhere('note.fileIds != \'{}\''); | ||||
| 						qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); | ||||
| 					})); | ||||
| 				} | ||||
|  | ||||
| 			// TODO: フィルタした結果件数が足りなかった場合の対応 | ||||
| 				if (ps.includeLocalRenotes === false) { | ||||
| 					query.andWhere(new Brackets(qb => { | ||||
| 						qb.orWhere('note.renoteUserHost IS NOT NULL'); | ||||
| 						qb.orWhere('note.renoteId IS NULL'); | ||||
| 						qb.orWhere('note.text IS NOT NULL'); | ||||
| 						qb.orWhere('note.fileIds != \'{}\''); | ||||
| 						qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); | ||||
| 					})); | ||||
| 				} | ||||
|  | ||||
| 			timeline.sort((a, b) => a.id > b.id ? -1 : 1); | ||||
| 				if (ps.withFiles) { | ||||
| 					query.andWhere('note.fileIds != \'{}\''); | ||||
| 				} | ||||
| 				//#endregion | ||||
|  | ||||
| 			process.nextTick(() => { | ||||
| 				this.activeUsersChart.read(me); | ||||
| 			}); | ||||
| 				const timeline = await query.limit(ps.limit).getMany(); | ||||
|  | ||||
| 			return await this.noteEntityService.packMany(timeline, me); | ||||
| 				process.nextTick(() => { | ||||
| 					this.activeUsersChart.read(me); | ||||
| 				}); | ||||
|  | ||||
| 				return await this.noteEntityService.packMany(timeline, me); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo