refactor(backend): UserEntityService.packMany()の高速化 (#13550)
* refactor(backend): UserEntityService.packMany()の高速化 * 修正
This commit is contained in:
		
							parent
							
								
									6d9c234cb6
								
							
						
					
					
						commit
						5c1d86b796
					
				|  | @ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; | |||
| import * as Redis from 'ioredis'; | ||||
| import _Ajv from 'ajv'; | ||||
| import { ModuleRef } from '@nestjs/core'; | ||||
| import { In } from 'typeorm'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import type { Packed } from '@/misc/json-schema.js'; | ||||
|  | @ -14,9 +15,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js'; | |||
| import { awaitAll } from '@/misc/prelude/await-all.js'; | ||||
| import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; | ||||
| import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; | ||||
| import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; | ||||
| import { MiNotification } from '@/models/Notification.js'; | ||||
| import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; | ||||
| import { | ||||
| 	birthdaySchema, | ||||
| 	descriptionSchema, | ||||
| 	localUsernameSchema, | ||||
| 	locationSchema, | ||||
| 	nameSchema, | ||||
| 	passwordSchema, | ||||
| } from '@/models/User.js'; | ||||
| import type { | ||||
| 	BlockingsRepository, | ||||
| 	FollowingsRepository, | ||||
| 	FollowRequestsRepository, | ||||
| 	MiFollowing, | ||||
| 	MiUserNotePining, | ||||
| 	MiUserProfile, | ||||
| 	MutingsRepository, | ||||
| 	NoteUnreadsRepository, | ||||
| 	RenoteMutingsRepository, | ||||
| 	UserMemoRepository, | ||||
| 	UserNotePiningsRepository, | ||||
| 	UserProfilesRepository, | ||||
| 	UserSecurityKeysRepository, | ||||
| 	UsersRepository, | ||||
| } from '@/models/_.js'; | ||||
| import { bindThis } from '@/decorators.js'; | ||||
| import { RoleService } from '@/core/RoleService.js'; | ||||
| import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; | ||||
|  | @ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { | |||
| 	return !isLocalUser(user); | ||||
| } | ||||
| 
 | ||||
| export type UserRelation = { | ||||
| 	id: MiUser['id'] | ||||
| 	following: MiFollowing | null, | ||||
| 	isFollowing: boolean | ||||
| 	isFollowed: boolean | ||||
| 	hasPendingFollowRequestFromYou: boolean | ||||
| 	hasPendingFollowRequestToYou: boolean | ||||
| 	isBlocking: boolean | ||||
| 	isBlocked: boolean | ||||
| 	isMuted: boolean | ||||
| 	isRenoteMuted: boolean | ||||
| } | ||||
| 
 | ||||
| @Injectable() | ||||
| export class UserEntityService implements OnModuleInit { | ||||
| 	private apPersonService: ApPersonService; | ||||
| 	private noteEntityService: NoteEntityService; | ||||
| 	private driveFileEntityService: DriveFileEntityService; | ||||
| 	private pageEntityService: PageEntityService; | ||||
| 	private customEmojiService: CustomEmojiService; | ||||
| 	private announcementService: AnnouncementService; | ||||
|  | @ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit { | |||
| 		@Inject(DI.renoteMutingsRepository) | ||||
| 		private renoteMutingsRepository: RenoteMutingsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.driveFilesRepository) | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.noteUnreadsRepository) | ||||
| 		private noteUnreadsRepository: NoteUnreadsRepository, | ||||
| 
 | ||||
|  | @ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit { | |||
| 		@Inject(DI.userProfilesRepository) | ||||
| 		private userProfilesRepository: UserProfilesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.announcementReadsRepository) | ||||
| 		private announcementReadsRepository: AnnouncementReadsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.announcementsRepository) | ||||
| 		private announcementsRepository: AnnouncementsRepository, | ||||
| 
 | ||||
| 		@Inject(DI.userMemosRepository) | ||||
| 		private userMemosRepository: UserMemoRepository, | ||||
| 	) { | ||||
|  | @ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit { | |||
| 	onModuleInit() { | ||||
| 		this.apPersonService = this.moduleRef.get('ApPersonService'); | ||||
| 		this.noteEntityService = this.moduleRef.get('NoteEntityService'); | ||||
| 		this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); | ||||
| 		this.pageEntityService = this.moduleRef.get('PageEntityService'); | ||||
| 		this.customEmojiService = this.moduleRef.get('CustomEmojiService'); | ||||
| 		this.announcementService = this.moduleRef.get('AnnouncementService'); | ||||
|  | @ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 	public isRemoteUser = isRemoteUser; | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async getRelation(me: MiUser['id'], target: MiUser['id']) { | ||||
| 	public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> { | ||||
| 		const [ | ||||
| 			following, | ||||
| 			isFollowed, | ||||
|  | @ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit { | |||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise<Map<MiUser['id'], UserRelation>> { | ||||
| 		const [ | ||||
| 			followers, | ||||
| 			followees, | ||||
| 			followersRequests, | ||||
| 			followeesRequests, | ||||
| 			blockers, | ||||
| 			blockees, | ||||
| 			muters, | ||||
| 			renoteMuters, | ||||
| 		] = await Promise.all([ | ||||
| 			this.followingsRepository.findBy({ followerId: me }) | ||||
| 				.then(f => new Map(f.map(it => [it.followeeId, it]))), | ||||
| 			this.followingsRepository.findBy({ followeeId: me }) | ||||
| 				.then(it => it.map(it => it.followerId)), | ||||
| 			this.followRequestsRepository.findBy({ followerId: me }) | ||||
| 				.then(it => it.map(it => it.followeeId)), | ||||
| 			this.followRequestsRepository.findBy({ followeeId: me }) | ||||
| 				.then(it => it.map(it => it.followerId)), | ||||
| 			this.blockingsRepository.findBy({ blockerId: me }) | ||||
| 				.then(it => it.map(it => it.blockeeId)), | ||||
| 			this.blockingsRepository.findBy({ blockeeId: me }) | ||||
| 				.then(it => it.map(it => it.blockerId)), | ||||
| 			this.mutingsRepository.findBy({ muterId: me }) | ||||
| 				.then(it => it.map(it => it.muteeId)), | ||||
| 			this.renoteMutingsRepository.findBy({ muterId: me }) | ||||
| 				.then(it => it.map(it => it.muteeId)), | ||||
| 		]); | ||||
| 
 | ||||
| 		return new Map( | ||||
| 			targets.map(target => { | ||||
| 				const following = followers.get(target) ?? null; | ||||
| 
 | ||||
| 				return [ | ||||
| 					target, | ||||
| 					{ | ||||
| 						id: target, | ||||
| 						following: following, | ||||
| 						isFollowing: following != null, | ||||
| 						isFollowed: followees.includes(target), | ||||
| 						hasPendingFollowRequestFromYou: followersRequests.includes(target), | ||||
| 						hasPendingFollowRequestToYou: followeesRequests.includes(target), | ||||
| 						isBlocking: blockers.includes(target), | ||||
| 						isBlocked: blockees.includes(target), | ||||
| 						isMuted: muters.includes(target), | ||||
| 						isRenoteMuted: renoteMuters.includes(target), | ||||
| 					}, | ||||
| 				]; | ||||
| 			}), | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	@bindThis | ||||
| 	public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> { | ||||
| 		/* | ||||
|  | @ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit { | |||
| 			schema?: S, | ||||
| 			includeSecrets?: boolean, | ||||
| 			userProfile?: MiUserProfile, | ||||
| 			userRelations?: Map<MiUser['id'], UserRelation>, | ||||
| 			userMemos?: Map<MiUser['id'], string | null>, | ||||
| 			pinNotes?: Map<MiUser['id'], MiUserNotePining[]>, | ||||
| 		}, | ||||
| 	): Promise<Packed<S>> { | ||||
| 		const opts = Object.assign({ | ||||
|  | @ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit { | |||
| 		const isMe = meId === user.id; | ||||
| 		const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; | ||||
| 
 | ||||
| 		const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null; | ||||
| 		const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin') | ||||
| 			.where('pin.userId = :userId', { userId: user.id }) | ||||
| 			.innerJoinAndSelect('pin.note', 'note') | ||||
| 			.orderBy('pin.id', 'DESC') | ||||
| 			.getMany() : []; | ||||
| 		const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; | ||||
| 		const profile = isDetailed | ||||
| 			? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) | ||||
| 			: null; | ||||
| 
 | ||||
| 		let relation: UserRelation | null = null; | ||||
| 		if (meId && !isMe && isDetailed) { | ||||
| 			if (opts.userRelations) { | ||||
| 				relation = opts.userRelations.get(user.id) ?? null; | ||||
| 			} else { | ||||
| 				relation = await this.getRelation(meId, user.id); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		let memo: string | null = null; | ||||
| 		if (isDetailed && meId) { | ||||
| 			if (opts.userMemos) { | ||||
| 				memo = opts.userMemos.get(user.id) ?? null; | ||||
| 			} else { | ||||
| 				memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id }) | ||||
| 					.then(row => row?.memo ?? null); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		let pins: MiUserNotePining[] = []; | ||||
| 		if (isDetailed) { | ||||
| 			if (opts.pinNotes) { | ||||
| 				pins = opts.pinNotes.get(user.id) ?? []; | ||||
| 			} else { | ||||
| 				pins = await this.userNotePiningsRepository.createQueryBuilder('pin') | ||||
| 					.where('pin.userId = :userId', { userId: user.id }) | ||||
| 					.innerJoinAndSelect('pin.note', 'note') | ||||
| 					.orderBy('pin.id', 'DESC') | ||||
| 					.getMany(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		const followingCount = profile == null ? null : | ||||
| 			(profile.followingVisibility === 'public') || isMe ? user.followingCount : | ||||
|  | @ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 				twoFactorEnabled: profile!.twoFactorEnabled, | ||||
| 				usePasswordLessLogin: profile!.usePasswordLessLogin, | ||||
| 				securityKeys: profile!.twoFactorEnabled | ||||
| 					? this.userSecurityKeysRepository.countBy({ | ||||
| 						userId: user.id, | ||||
| 					}).then(result => result >= 1) | ||||
| 					? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) | ||||
| 					: false, | ||||
| 				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ | ||||
| 					id: role.id, | ||||
|  | @ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 					isAdministrator: role.isAdministrator, | ||||
| 					displayOrder: role.displayOrder, | ||||
| 				}))), | ||||
| 				memo: meId == null ? null : await this.userMemosRepository.findOneBy({ | ||||
| 					userId: meId, | ||||
| 					targetUserId: user.id, | ||||
| 				}).then(row => row?.memo ?? null), | ||||
| 				memo: memo, | ||||
| 				moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, | ||||
| 			} : {}), | ||||
| 
 | ||||
|  | @ -514,7 +617,7 @@ export class UserEntityService implements OnModuleInit { | |||
| 		return await awaitAll(packed); | ||||
| 	} | ||||
| 
 | ||||
| 	public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>( | ||||
| 	public async packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>( | ||||
| 		users: (MiUser['id'] | MiUser)[], | ||||
| 		me?: { id: MiUser['id'] } | null | undefined, | ||||
| 		options?: { | ||||
|  | @ -522,6 +625,70 @@ export class UserEntityService implements OnModuleInit { | |||
| 			includeSecrets?: boolean, | ||||
| 		}, | ||||
| 	): Promise<Packed<S>[]> { | ||||
| 		return Promise.all(users.map(u => this.pack(u, me, options))); | ||||
| 		// -- IDのみの要素を補完して完全なエンティティ一覧を作る
 | ||||
| 
 | ||||
| 		const _users = users.filter((user): user is MiUser => typeof user !== 'string'); | ||||
| 		if (_users.length !== users.length) { | ||||
| 			_users.push( | ||||
| 				...await this.usersRepository.findBy({ | ||||
| 					id: In(users.filter((user): user is string => typeof user === 'string')), | ||||
| 				}), | ||||
| 			); | ||||
| 		} | ||||
| 		const _userIds = _users.map(u => u.id); | ||||
| 
 | ||||
| 		// -- 特に前提条件のない値群を取得
 | ||||
| 
 | ||||
| 		const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) | ||||
| 			.then(profiles => new Map(profiles.map(p => [p.userId, p]))); | ||||
| 
 | ||||
| 		// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
 | ||||
| 
 | ||||
| 		let userRelations: Map<MiUser['id'], UserRelation> = new Map(); | ||||
| 		let userMemos: Map<MiUser['id'], string | null> = new Map(); | ||||
| 		let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map(); | ||||
| 
 | ||||
| 		if (options?.schema !== 'UserLite') { | ||||
| 			const meId = me ? me.id : null; | ||||
| 			if (meId) { | ||||
| 				userMemos = await this.userMemosRepository.findBy({ userId: meId }) | ||||
| 					.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))); | ||||
| 
 | ||||
| 				if (_userIds.length > 0) { | ||||
| 					userRelations = await this.getRelations(meId, _userIds); | ||||
| 					pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin') | ||||
| 						.where('pin.userId IN (:...userIds)', { userIds: _userIds }) | ||||
| 						.innerJoinAndSelect('pin.note', 'note') | ||||
| 						.getMany() | ||||
| 						.then(pinsNotes => { | ||||
| 							const map = new Map<MiUser['id'], MiUserNotePining[]>(); | ||||
| 							for (const note of pinsNotes) { | ||||
| 								const notes = map.get(note.userId) ?? []; | ||||
| 								notes.push(note); | ||||
| 								map.set(note.userId, notes); | ||||
| 							} | ||||
| 							for (const [, notes] of map.entries()) { | ||||
| 								// pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
 | ||||
| 								notes.sort((a, b) => b.id.localeCompare(a.id)); | ||||
| 							} | ||||
| 							return map; | ||||
| 						}); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return Promise.all( | ||||
| 			_users.map(u => this.pack( | ||||
| 				u, | ||||
| 				me, | ||||
| 				{ | ||||
| 					...options, | ||||
| 					userProfile: profilesMap.get(u.id), | ||||
| 					userRelations: userRelations, | ||||
| 					userMemos: userMemos, | ||||
| 					pinNotes: pinNotes, | ||||
| 				}, | ||||
| 			)), | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		private userEntityService: UserEntityService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; | ||||
| 
 | ||||
| 			const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); | ||||
| 
 | ||||
| 			return Array.isArray(ps.userId) ? relations : relations[0]; | ||||
| 			return Array.isArray(ps.userId) | ||||
| 				? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) | ||||
| 				: await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,528 @@ | |||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and misskey-project | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
| 
 | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| import { GlobalModule } from '@/GlobalModule.js'; | ||||
| import { CoreModule } from '@/core/CoreModule.js'; | ||||
| import type { MiUser } from '@/models/User.js'; | ||||
| import { secureRndstr } from '@/misc/secure-rndstr.js'; | ||||
| import { genAidx } from '@/misc/id/aidx.js'; | ||||
| import { | ||||
| 	BlockingsRepository, | ||||
| 	FollowingsRepository, FollowRequestsRepository, | ||||
| 	MiUserProfile, MutingsRepository, RenoteMutingsRepository, | ||||
| 	UserMemoRepository, | ||||
| 	UserProfilesRepository, | ||||
| 	UsersRepository, | ||||
| } from '@/models/_.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; | ||||
| import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; | ||||
| import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; | ||||
| import { PageEntityService } from '@/core/entities/PageEntityService.js'; | ||||
| import { CustomEmojiService } from '@/core/CustomEmojiService.js'; | ||||
| import { AnnouncementService } from '@/core/AnnouncementService.js'; | ||||
| import { RoleService } from '@/core/RoleService.js'; | ||||
| import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; | ||||
| import { IdService } from '@/core/IdService.js'; | ||||
| import { UtilityService } from '@/core/UtilityService.js'; | ||||
| import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; | ||||
| import { ModerationLogService } from '@/core/ModerationLogService.js'; | ||||
| import { GlobalEventService } from '@/core/GlobalEventService.js'; | ||||
| import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; | ||||
| import { MetaService } from '@/core/MetaService.js'; | ||||
| import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; | ||||
| import { CacheService } from '@/core/CacheService.js'; | ||||
| import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; | ||||
| import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; | ||||
| import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; | ||||
| import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; | ||||
| import { MfmService } from '@/core/MfmService.js'; | ||||
| import { HashtagService } from '@/core/HashtagService.js'; | ||||
| import UsersChart from '@/core/chart/charts/users.js'; | ||||
| import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; | ||||
| import InstanceChart from '@/core/chart/charts/instance.js'; | ||||
| import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; | ||||
| import { AccountMoveService } from '@/core/AccountMoveService.js'; | ||||
| import { ReactionService } from '@/core/ReactionService.js'; | ||||
| import { NotificationService } from '@/core/NotificationService.js'; | ||||
| 
 | ||||
| process.env.NODE_ENV = 'test'; | ||||
| 
 | ||||
| describe('UserEntityService', () => { | ||||
| 	describe('pack/packMany', () => { | ||||
| 		let app: TestingModule; | ||||
| 		let service: UserEntityService; | ||||
| 		let usersRepository: UsersRepository; | ||||
| 		let userProfileRepository: UserProfilesRepository; | ||||
| 		let userMemosRepository: UserMemoRepository; | ||||
| 		let followingRepository: FollowingsRepository; | ||||
| 		let followingRequestRepository: FollowRequestsRepository; | ||||
| 		let blockingRepository: BlockingsRepository; | ||||
| 		let mutingRepository: MutingsRepository; | ||||
| 		let renoteMutingsRepository: RenoteMutingsRepository; | ||||
| 
 | ||||
| 		async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) { | ||||
| 			const un = secureRndstr(16); | ||||
| 			const user = await usersRepository | ||||
| 				.insert({ | ||||
| 					...userData, | ||||
| 					id: genAidx(Date.now()), | ||||
| 					username: un, | ||||
| 					usernameLower: un, | ||||
| 				}) | ||||
| 				.then(x => usersRepository.findOneByOrFail(x.identifiers[0])); | ||||
| 
 | ||||
| 			await userProfileRepository.insert({ | ||||
| 				...profileData, | ||||
| 				userId: user.id, | ||||
| 			}); | ||||
| 
 | ||||
| 			return user; | ||||
| 		} | ||||
| 
 | ||||
| 		async function memo(writer: MiUser, target: MiUser, memo: string) { | ||||
| 			await userMemosRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				userId: writer.id, | ||||
| 				targetUserId: target.id, | ||||
| 				memo, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		async function follow(follower: MiUser, followee: MiUser) { | ||||
| 			await followingRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				followerId: follower.id, | ||||
| 				followeeId: followee.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		async function requestFollow(requester: MiUser, requestee: MiUser) { | ||||
| 			await followingRequestRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				followerId: requester.id, | ||||
| 				followeeId: requestee.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		async function block(blocker: MiUser, blockee: MiUser) { | ||||
| 			await blockingRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				blockerId: blocker.id, | ||||
| 				blockeeId: blockee.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		async function mute(mutant: MiUser, mutee: MiUser) { | ||||
| 			await mutingRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				muterId: mutant.id, | ||||
| 				muteeId: mutee.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		async function muteRenote(mutant: MiUser, mutee: MiUser) { | ||||
| 			await renoteMutingsRepository.insert({ | ||||
| 				id: genAidx(Date.now()), | ||||
| 				muterId: mutant.id, | ||||
| 				muteeId: mutee.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		function randomIntRange(weight = 10) { | ||||
| 			return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); | ||||
| 		} | ||||
| 
 | ||||
| 		beforeAll(async () => { | ||||
| 			const services = [ | ||||
| 				UserEntityService, | ||||
| 				ApPersonService, | ||||
| 				NoteEntityService, | ||||
| 				PageEntityService, | ||||
| 				CustomEmojiService, | ||||
| 				AnnouncementService, | ||||
| 				RoleService, | ||||
| 				FederatedInstanceService, | ||||
| 				IdService, | ||||
| 				AvatarDecorationService, | ||||
| 				UtilityService, | ||||
| 				EmojiEntityService, | ||||
| 				ModerationLogService, | ||||
| 				GlobalEventService, | ||||
| 				DriveFileEntityService, | ||||
| 				MetaService, | ||||
| 				FetchInstanceMetadataService, | ||||
| 				CacheService, | ||||
| 				ApResolverService, | ||||
| 				ApNoteService, | ||||
| 				ApImageService, | ||||
| 				ApMfmService, | ||||
| 				MfmService, | ||||
| 				HashtagService, | ||||
| 				UsersChart, | ||||
| 				ChartLoggerService, | ||||
| 				InstanceChart, | ||||
| 				ApLoggerService, | ||||
| 				AccountMoveService, | ||||
| 				ReactionService, | ||||
| 				NotificationService, | ||||
| 			]; | ||||
| 
 | ||||
| 			app = await Test.createTestingModule({ | ||||
| 				imports: [GlobalModule, CoreModule], | ||||
| 				providers: [ | ||||
| 					...services, | ||||
| 					...services.map(x => ({ provide: x.name, useExisting: x })), | ||||
| 				], | ||||
| 			}).compile(); | ||||
| 			await app.init(); | ||||
| 			app.enableShutdownHooks(); | ||||
| 
 | ||||
| 			service = app.get<UserEntityService>(UserEntityService); | ||||
| 			usersRepository = app.get<UsersRepository>(DI.usersRepository); | ||||
| 			userProfileRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository); | ||||
| 			userMemosRepository = app.get<UserMemoRepository>(DI.userMemosRepository); | ||||
| 			followingRepository = app.get<FollowingsRepository>(DI.followingsRepository); | ||||
| 			followingRequestRepository = app.get<FollowRequestsRepository>(DI.followRequestsRepository); | ||||
| 			blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository); | ||||
| 			mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository); | ||||
| 			renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository); | ||||
| 		}); | ||||
| 
 | ||||
| 		afterAll(async () => { | ||||
| 			await app.close(); | ||||
| 		}); | ||||
| 
 | ||||
| 		test('UserLite', async() => { | ||||
| 			const me = await createUser(); | ||||
| 			const who = await createUser(); | ||||
| 
 | ||||
| 			await memo(me, who, 'memo'); | ||||
| 
 | ||||
| 			const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; | ||||
| 			// no detail
 | ||||
| 			expect(actual.memo).toBeUndefined(); | ||||
| 			// no detail and me
 | ||||
| 			expect(actual.birthday).toBeUndefined(); | ||||
| 			// no detail and me
 | ||||
| 			expect(actual.achievements).toBeUndefined(); | ||||
| 		}); | ||||
| 
 | ||||
| 		test('UserDetailedNotMe', async() => { | ||||
| 			const me = await createUser(); | ||||
| 			const who = await createUser({}, { birthday: '2000-01-01' }); | ||||
| 
 | ||||
| 			await memo(me, who, 'memo'); | ||||
| 
 | ||||
| 			const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; | ||||
| 			// is detail
 | ||||
| 			expect(actual.memo).toBe('memo'); | ||||
| 			// is detail
 | ||||
| 			expect(actual.birthday).toBe('2000-01-01'); | ||||
| 			// no detail and me
 | ||||
| 			expect(actual.achievements).toBeUndefined(); | ||||
| 		}); | ||||
| 
 | ||||
| 		test('MeDetailed', async() => { | ||||
| 			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }]; | ||||
| 			const me = await createUser({}, { | ||||
| 				birthday: '2000-01-01', | ||||
| 				achievements: achievements, | ||||
| 			}); | ||||
| 			await memo(me, me, 'memo'); | ||||
| 
 | ||||
| 			const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; | ||||
| 			// is detail
 | ||||
| 			expect(actual.memo).toBe('memo'); | ||||
| 			// is detail
 | ||||
| 			expect(actual.birthday).toBe('2000-01-01'); | ||||
| 			// is detail and me
 | ||||
| 			expect(actual.achievements).toEqual(achievements); | ||||
| 		}); | ||||
| 
 | ||||
| 		describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { | ||||
| 			test('no-preload', async() => { | ||||
| 				const me = await createUser(); | ||||
| 				// meがフォローしてる人たち
 | ||||
| 				const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of followeeMe) { | ||||
| 					await follow(me, who); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(true); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meをフォローしてる人たち
 | ||||
| 				const followerMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of followerMe) { | ||||
| 					await follow(who, me); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(true); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meがフォローリクエストを送った人たち
 | ||||
| 				const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of requestsFromYou) { | ||||
| 					await requestFollow(me, who); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(true); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meにフォローリクエストを送った人たち
 | ||||
| 				const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of requestsToYou) { | ||||
| 					await requestFollow(who, me); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(true); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meがブロックしてる人たち
 | ||||
| 				const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of blockingYou) { | ||||
| 					await block(me, who); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(true); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meをブロックしてる人たち
 | ||||
| 				const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of blockingMe) { | ||||
| 					await block(who, me); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(true); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meがミュートしてる人たち
 | ||||
| 				const muters = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of muters) { | ||||
| 					await mute(me, who); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(true); | ||||
| 					expect(actual.isRenoteMuted).toBe(false); | ||||
| 				} | ||||
| 
 | ||||
| 				// meがリノートミュートしてる人たち
 | ||||
| 				const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 				for (const who of renoteMuters) { | ||||
| 					await muteRenote(me, who); | ||||
| 					const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; | ||||
| 					expect(actual.isFollowing).toBe(false); | ||||
| 					expect(actual.isFollowed).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 					expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 					expect(actual.isBlocking).toBe(false); | ||||
| 					expect(actual.isBlocked).toBe(false); | ||||
| 					expect(actual.isMuted).toBe(false); | ||||
| 					expect(actual.isRenoteMuted).toBe(true); | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			test('preload', async() => { | ||||
| 				const me = await createUser(); | ||||
| 
 | ||||
| 				{ | ||||
| 					// meがフォローしてる人たち
 | ||||
| 					const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of followeeMe) { | ||||
| 						await follow(me, who); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(true); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meをフォローしてる人たち
 | ||||
| 					const followerMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of followerMe) { | ||||
| 						await follow(who, me); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(true); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meがフォローリクエストを送った人たち
 | ||||
| 					const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of requestsFromYou) { | ||||
| 						await requestFollow(me, who); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(true); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meにフォローリクエストを送った人たち
 | ||||
| 					const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of requestsToYou) { | ||||
| 						await requestFollow(who, me); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(true); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meがブロックしてる人たち
 | ||||
| 					const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of blockingYou) { | ||||
| 						await block(me, who); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(true); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meをブロックしてる人たち
 | ||||
| 					const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of blockingMe) { | ||||
| 						await block(who, me); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(true); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meがミュートしてる人たち
 | ||||
| 					const muters = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of muters) { | ||||
| 						await mute(me, who); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(true); | ||||
| 						expect(actual.isRenoteMuted).toBe(false); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				{ | ||||
| 					// meがリノートミュートしてる人たち
 | ||||
| 					const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); | ||||
| 					for (const who of renoteMuters) { | ||||
| 						await muteRenote(me, who); | ||||
| 					} | ||||
| 					const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; | ||||
| 					for (const actual of actualList) { | ||||
| 						expect(actual.isFollowing).toBe(false); | ||||
| 						expect(actual.isFollowed).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestFromYou).toBe(false); | ||||
| 						expect(actual.hasPendingFollowRequestToYou).toBe(false); | ||||
| 						expect(actual.isBlocking).toBe(false); | ||||
| 						expect(actual.isBlocked).toBe(false); | ||||
| 						expect(actual.isMuted).toBe(false); | ||||
| 						expect(actual.isRenoteMuted).toBe(true); | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue