Merge remote-tracking branch 'refs/remotes/misskey-original/develop' into develop

# Conflicts:
#	packages/backend/src/core/entities/UserEntityService.ts
This commit is contained in:
mattyatea 2024-05-11 00:00:52 +09:00
commit e270586662
1 changed files with 217 additions and 32 deletions

View File

@ -14,9 +14,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } 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 { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; import {
import { MiNotification } from '@/models/Notification.js'; birthdaySchema,
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; 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 { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@ -46,11 +67,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
return !isLocalUser(user); 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() @Injectable()
export class UserEntityService implements OnModuleInit { export class UserEntityService implements OnModuleInit {
private apPersonService: ApPersonService; private apPersonService: ApPersonService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private driveFileEntityService: DriveFileEntityService;
private pageEntityService: PageEntityService; private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
private announcementService: AnnouncementService; private announcementService: AnnouncementService;
@ -89,9 +122,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.renoteMutingsRepository) @Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository, private renoteMutingsRepository: RenoteMutingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.noteUnreadsRepository) @Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository, private noteUnreadsRepository: NoteUnreadsRepository,
@ -101,12 +131,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.userMemosRepository) @Inject(DI.userMemosRepository)
private userMemosRepository: UserMemoRepository, private userMemosRepository: UserMemoRepository,
) { ) {
@ -115,7 +139,6 @@ export class UserEntityService implements OnModuleInit {
onModuleInit() { onModuleInit() {
this.apPersonService = this.moduleRef.get('ApPersonService'); this.apPersonService = this.moduleRef.get('ApPersonService');
this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.pageEntityService = this.moduleRef.get('PageEntityService'); this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.announcementService = this.moduleRef.get('AnnouncementService'); this.announcementService = this.moduleRef.get('AnnouncementService');
@ -138,7 +161,7 @@ export class UserEntityService implements OnModuleInit {
public isRemoteUser = isRemoteUser; public isRemoteUser = isRemoteUser;
@bindThis @bindThis
public async getRelation(me: MiUser['id'], target: MiUser['id']) { public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise<UserRelation> {
const [ const [
following, following,
isFollowed, isFollowed,
@ -211,6 +234,80 @@ 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.createQueryBuilder('f')
.select('f.followerId')
.where('f.followeeId = :me', { me })
.getRawMany<{ f_followerId: string }>()
.then(it => it.map(it => it.f_followerId)),
this.followRequestsRepository.createQueryBuilder('f')
.select('f.followeeId')
.where('f.followerId = :me', { me })
.getRawMany<{ f_followeeId: string }>()
.then(it => it.map(it => it.f_followeeId)),
this.followRequestsRepository.createQueryBuilder('f')
.select('f.followerId')
.where('f.followeeId = :me', { me })
.getRawMany<{ f_followerId: string }>()
.then(it => it.map(it => it.f_followerId)),
this.blockingsRepository.createQueryBuilder('b')
.select('b.blockeeId')
.where('b.blockerId = :me', { me })
.getRawMany<{ b_blockeeId: string }>()
.then(it => it.map(it => it.b_blockeeId)),
this.blockingsRepository.createQueryBuilder('b')
.select('b.blockerId')
.where('b.blockeeId = :me', { me })
.getRawMany<{ b_blockerId: string }>()
.then(it => it.map(it => it.b_blockerId)),
this.mutingsRepository.createQueryBuilder('m')
.select('m.muteeId')
.where('m.muterId = :me', { me })
.getRawMany<{ m_muteeId: string }>()
.then(it => it.map(it => it.m_muteeId)),
this.renoteMutingsRepository.createQueryBuilder('m')
.select('m.muteeId')
.where('m.muterId = :me', { me })
.getRawMany<{ m_muteeId: string }>()
.then(it => it.map(it => it.m_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 @bindThis
public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> { public async getHasUnreadAntenna(userId: MiUser['id']): Promise<boolean> {
/* /*
@ -303,6 +400,9 @@ export class UserEntityService implements OnModuleInit {
schema?: S, schema?: S,
includeSecrets?: boolean, includeSecrets?: boolean,
userProfile?: MiUserProfile, userProfile?: MiUserProfile,
userRelations?: Map<MiUser['id'], UserRelation>,
userMemos?: Map<MiUser['id'], string | null>,
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
}, },
): Promise<Packed<S>> { ): Promise<Packed<S>> {
const opts = Object.assign({ const opts = Object.assign({
@ -317,13 +417,41 @@ export class UserEntityService implements OnModuleInit {
const isMe = meId === user.id; const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null; const profile = isDetailed
const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin') ? (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 }) .where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note') .innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC') .orderBy('pin.id', 'DESC')
.getMany() : []; .getMany();
const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; }
}
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount : (profile.followingVisibility === 'public') || isMe ? user.followingCount :
@ -362,7 +490,6 @@ export class UserEntityService implements OnModuleInit {
}))) : [], }))) : [],
isBot: user.isBot, isBot: user.isBot,
isCat: user.isCat, isCat: user.isCat,
isGorilla: user.isGorilla,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name, name: instance.name,
softwareName: instance.softwareName, softwareName: instance.softwareName,
@ -417,9 +544,7 @@ export class UserEntityService implements OnModuleInit {
twoFactorEnabled: profile!.twoFactorEnabled, twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin, usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled securityKeys: profile!.twoFactorEnabled
? this.userSecurityKeysRepository.countBy({ ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
userId: user.id,
}).then(result => result >= 1)
: false, : false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ 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, id: role.id,
@ -431,10 +556,7 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder, displayOrder: role.displayOrder,
}))), }))),
memo: meId == null ? null : await this.userMemosRepository.findOneBy({ memo: memo,
userId: meId,
targetUserId: user.id,
}).then(row => row?.memo ?? null),
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}), } : {}),
@ -515,7 +637,7 @@ export class UserEntityService implements OnModuleInit {
return await awaitAll(packed); 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)[], users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: {
@ -523,6 +645,69 @@ export class UserEntityService implements OnModuleInit {
includeSecrets?: boolean, includeSecrets?: boolean,
}, },
): Promise<Packed<S>[]> { ): 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);
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
let profilesMap: Map<MiUser['id'], MiUserProfile> = new Map();
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') {
profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
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,
},
)),
);
} }
} }