wip
This commit is contained in:
		| @@ -105,28 +105,6 @@ export class QueryService { | ||||
| 		q.setParameters(blockedQuery.getParameters()); | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public generateChannelQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void { | ||||
| 		if (me == null) { | ||||
| 			q.andWhere('note.channelId IS NULL'); | ||||
| 		} else { | ||||
| 			q.leftJoinAndSelect('note.channel', 'channel'); | ||||
|  | ||||
| 			const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing') | ||||
| 				.select('channelFollowing.followeeId') | ||||
| 				.where('channelFollowing.followerId = :followerId', { followerId: me.id }); | ||||
|  | ||||
| 			q.andWhere(new Brackets(qb => { qb | ||||
| 				// チャンネルのノートではない | ||||
| 				.where('note.channelId IS NULL') | ||||
| 				// または自分がフォローしているチャンネルのノート | ||||
| 				.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); | ||||
| 			})); | ||||
|  | ||||
| 			q.setParameters(channelFollowingQuery.getParameters()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public generateMutedNoteThreadQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void { | ||||
| 		const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted') | ||||
| @@ -198,32 +176,6 @@ export class QueryService { | ||||
| 		q.setParameters(mutingQuery.getParameters()); | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<MiUser, 'id'> | null): void { | ||||
| 		if (me == null) { | ||||
| 			q.andWhere(new Brackets(qb => { qb | ||||
| 				.where('note.replyId IS NULL') // 返信ではない | ||||
| 				.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 | ||||
| 					.where('note.replyId IS NOT NULL') | ||||
| 					.andWhere('note.replyUserId = note.userId'); | ||||
| 				})); | ||||
| 			})); | ||||
| 		} else if (!withReplies) { | ||||
| 			q.andWhere(new Brackets(qb => { qb | ||||
| 				.where('note.replyId IS NULL') // 返信ではない | ||||
| 				.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信 | ||||
| 				.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信 | ||||
| 					.where('note.replyId IS NOT NULL') | ||||
| 					.andWhere('note.userId = :meId', { meId: me.id }); | ||||
| 				})) | ||||
| 				.orWhere(new Brackets(qb => { qb // 返信だけど投稿者自身への返信 | ||||
| 					.where('note.replyId IS NOT NULL') | ||||
| 					.andWhere('note.replyUserId = note.userId'); | ||||
| 				})); | ||||
| 			})); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void { | ||||
| 		// This code must always be synchronized with the checks in Notes.isVisibleForMe. | ||||
|   | ||||
| @@ -68,48 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				throw new ApiError(meta.errors.gtlDisabled); | ||||
| 			} | ||||
|  | ||||
| 			//#region Construct query | ||||
| 			const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), | ||||
| 				ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) | ||||
| 				.andWhere('note.visibility = \'public\'') | ||||
| 				.andWhere('note.channelId IS NULL') | ||||
| 				.innerJoinAndSelect('note.user', 'user') | ||||
| 				.leftJoinAndSelect('note.reply', 'reply') | ||||
| 				.leftJoinAndSelect('note.renote', 'renote') | ||||
| 				.leftJoinAndSelect('reply.user', 'replyUser') | ||||
| 				.leftJoinAndSelect('renote.user', 'renoteUser'); | ||||
|  | ||||
| 			this.queryService.generateRepliesQuery(query, ps.withReplies, me); | ||||
| 			if (me) { | ||||
| 				this.queryService.generateMutedUserQuery(query, me); | ||||
| 				this.queryService.generateBlockedUserQuery(query, me); | ||||
| 				this.queryService.generateMutedUserRenotesQueryForNotes(query, me); | ||||
| 			} | ||||
|  | ||||
| 			if (ps.withFiles) { | ||||
| 				query.andWhere('note.fileIds != \'{}\''); | ||||
| 			} | ||||
|  | ||||
| 			if (ps.withRenotes === false) { | ||||
| 				query.andWhere(new Brackets(qb => { | ||||
| 					qb.orWhere('note.renoteId IS NULL'); | ||||
| 					qb.orWhere(new Brackets(qb => { | ||||
| 						qb.orWhere('note.text IS NOT NULL'); | ||||
| 						qb.orWhere('note.fileIds != \'{}\''); | ||||
| 					})); | ||||
| 				})); | ||||
| 			} | ||||
| 			//#endregion | ||||
|  | ||||
| 			const timeline = await query.limit(ps.limit).getMany(); | ||||
|  | ||||
| 			process.nextTick(() => { | ||||
| 				if (me) { | ||||
| 					this.activeUsersChart.read(me); | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
| 			return await this.noteEntityService.packMany(timeline, me); | ||||
| 			// TODO? | ||||
| 			return []; | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,14 +5,16 @@ | ||||
|  | ||||
| import { Brackets } from 'typeorm'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; | ||||
| import * as Redis from 'ioredis'; | ||||
| import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { QueryService } from '@/core/QueryService.js'; | ||||
| import ActiveUsersChart from '@/core/chart/charts/active-users.js'; | ||||
| 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 { ApiError } from '../../error.js'; | ||||
|  | ||||
| export const meta = { | ||||
| @@ -60,17 +62,17 @@ export const paramDef = { | ||||
| @Injectable() | ||||
| export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export | ||||
| 	constructor( | ||||
| 		@Inject(DI.redis) | ||||
| 		private redisClient: Redis.Redis, | ||||
|  | ||||
| 		@Inject(DI.notesRepository) | ||||
| 		private notesRepository: NotesRepository, | ||||
|  | ||||
| 		@Inject(DI.followingsRepository) | ||||
| 		private followingsRepository: FollowingsRepository, | ||||
|  | ||||
| 		private noteEntityService: NoteEntityService, | ||||
| 		private queryService: QueryService, | ||||
| 		private roleService: RoleService, | ||||
| 		private activeUsersChart: ActiveUsersChart, | ||||
| 		private idService: IdService, | ||||
| 		private cacheService: CacheService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const policies = await this.roleService.getUserPolicies(me.id); | ||||
| @@ -78,78 +80,73 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				throw new ApiError(meta.errors.stlDisabled); | ||||
| 			} | ||||
|  | ||||
| 			//#region Construct query | ||||
| 			const followingQuery = this.followingsRepository.createQueryBuilder('following') | ||||
| 				.select('following.followeeId') | ||||
| 				.where('following.followerId = :followerId', { followerId: me.id }); | ||||
| 			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), | ||||
| 			]); | ||||
|  | ||||
| 			const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), | ||||
| 				ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) | ||||
| 				.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで | ||||
| 				.andWhere(new Brackets(qb => { | ||||
| 					qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id }) | ||||
| 						.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); | ||||
| 				})) | ||||
| 			let timeline: MiNote[] = []; | ||||
|  | ||||
| 			const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 | ||||
| 			let htlNoteIdsRes: [string, string[]][] = []; | ||||
| 			let ltlNoteIdsRes: [string, string[]][] = []; | ||||
|  | ||||
| 			if (!ps.sinceId && !ps.sinceDate) { | ||||
| 				htlNoteIdsRes = await this.redisClient.xrevrange( | ||||
| 					ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, | ||||
| 					ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', | ||||
| 					'-', | ||||
| 					'COUNT', limit); | ||||
| 				ltlNoteIdsRes = await this.redisClient.xrevrange( | ||||
| 					ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline', | ||||
| 					ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', | ||||
| 					'-', | ||||
| 					'COUNT', limit); | ||||
| 			} | ||||
|  | ||||
| 			const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); | ||||
| 			const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); | ||||
| 			const noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds])); | ||||
|  | ||||
| 			if (noteIds.length === 0) { | ||||
| 				return []; | ||||
| 			} | ||||
|  | ||||
| 			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') | ||||
| 				.setParameters(followingQuery.getParameters()); | ||||
| 				.leftJoinAndSelect('note.channel', 'channel'); | ||||
|  | ||||
| 			this.queryService.generateChannelQuery(query, me); | ||||
| 			this.queryService.generateRepliesQuery(query, ps.withReplies, me); | ||||
| 			this.queryService.generateVisibilityQuery(query, me); | ||||
| 			this.queryService.generateMutedUserQuery(query, me); | ||||
| 			this.queryService.generateBlockedUserQuery(query, me); | ||||
| 			this.queryService.generateMutedUserRenotesQueryForNotes(query, me); | ||||
| 			timeline = await query.getMany(); | ||||
|  | ||||
| 			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)'); | ||||
| 				})); | ||||
| 			} | ||||
| 			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; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			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)'); | ||||
| 				})); | ||||
| 			} | ||||
| 				return true; | ||||
| 			}); | ||||
|  | ||||
| 			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)'); | ||||
| 				})); | ||||
| 			} | ||||
| 			// TODO: フィルタした結果件数が足りなかった場合の対応 | ||||
|  | ||||
| 			if (ps.withFiles) { | ||||
| 				query.andWhere('note.fileIds != \'{}\''); | ||||
| 			} | ||||
|  | ||||
| 			if (ps.withRenotes === false) { | ||||
| 				query.andWhere(new Brackets(qb => { | ||||
| 					qb.orWhere('note.renoteId IS NULL'); | ||||
| 					qb.orWhere(new Brackets(qb => { | ||||
| 						qb.orWhere('note.text IS NOT NULL'); | ||||
| 						qb.orWhere('note.fileIds != \'{}\''); | ||||
| 					})); | ||||
| 				})); | ||||
| 			} | ||||
| 			//#endregion | ||||
|  | ||||
| 			const timeline = await query.limit(ps.limit).getMany(); | ||||
| 			timeline.sort((a, b) => a.id > b.id ? -1 : 1); | ||||
|  | ||||
| 			process.nextTick(() => { | ||||
| 				this.activeUsersChart.read(me); | ||||
|   | ||||
| @@ -132,6 +132,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				return true; | ||||
| 			}); | ||||
|  | ||||
| 			timeline.sort((a, b) => a.id > b.id ? -1 : 1); | ||||
|  | ||||
| 			process.nextTick(() => { | ||||
| 				if (me) { | ||||
| 					this.activeUsersChart.read(me); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo