@@ -7,16 +7,38 @@ import { Inject, Injectable } from '@nestjs/common';
7
7
import * as Redis from 'ioredis' ;
8
8
import _Ajv from 'ajv' ;
9
9
import { ModuleRef } from '@nestjs/core' ;
10
+ import { In } from 'typeorm' ;
10
11
import { DI } from '@/di-symbols.js' ;
11
12
import type { Config } from '@/config.js' ;
12
13
import type { Packed } from '@/misc/json-schema.js' ;
13
14
import type { Promiseable } from '@/misc/prelude/await-all.js' ;
14
15
import { awaitAll } from '@/misc/prelude/await-all.js' ;
15
16
import { USER_ACTIVE_THRESHOLD , USER_ONLINE_THRESHOLD } from '@/const.js' ;
16
17
import type { MiLocalUser , MiPartialLocalUser , MiPartialRemoteUser , MiRemoteUser , MiUser } from '@/models/User.js' ;
17
- import { birthdaySchema , descriptionSchema , localUsernameSchema , locationSchema , nameSchema , passwordSchema } from '@/models/User.js' ;
18
- import { MiNotification } from '@/models/Notification.js' ;
19
- import type { UsersRepository , UserSecurityKeysRepository , FollowingsRepository , FollowRequestsRepository , BlockingsRepository , MutingsRepository , DriveFilesRepository , NoteUnreadsRepository , UserNotePiningsRepository , UserProfilesRepository , AnnouncementReadsRepository , AnnouncementsRepository , MiUserProfile , RenoteMutingsRepository , UserMemoRepository } from '@/models/_.js' ;
18
+ import {
19
+ birthdaySchema ,
20
+ descriptionSchema ,
21
+ localUsernameSchema ,
22
+ locationSchema ,
23
+ nameSchema ,
24
+ passwordSchema ,
25
+ } from '@/models/User.js' ;
26
+ import type {
27
+ BlockingsRepository ,
28
+ FollowingsRepository ,
29
+ FollowRequestsRepository ,
30
+ MiFollowing ,
31
+ MiUserNotePining ,
32
+ MiUserProfile ,
33
+ MutingsRepository ,
34
+ NoteUnreadsRepository ,
35
+ RenoteMutingsRepository ,
36
+ UserMemoRepository ,
37
+ UserNotePiningsRepository ,
38
+ UserProfilesRepository ,
39
+ UserSecurityKeysRepository ,
40
+ UsersRepository ,
41
+ } from '@/models/_.js' ;
20
42
import { bindThis } from '@/decorators.js' ;
21
43
import { RoleService } from '@/core/RoleService.js' ;
22
44
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js' ;
@@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean {
46
68
return ! isLocalUser ( user ) ;
47
69
}
48
70
71
+ export type UserRelation = {
72
+ id : MiUser [ 'id' ]
73
+ following : MiFollowing | null ,
74
+ isFollowing : boolean
75
+ isFollowed : boolean
76
+ hasPendingFollowRequestFromYou : boolean
77
+ hasPendingFollowRequestToYou : boolean
78
+ isBlocking : boolean
79
+ isBlocked : boolean
80
+ isMuted : boolean
81
+ isRenoteMuted : boolean
82
+ }
83
+
49
84
@Injectable ( )
50
85
export class UserEntityService implements OnModuleInit {
51
86
private apPersonService : ApPersonService ;
52
87
private noteEntityService : NoteEntityService ;
53
- private driveFileEntityService : DriveFileEntityService ;
54
88
private pageEntityService : PageEntityService ;
55
89
private customEmojiService : CustomEmojiService ;
56
90
private announcementService : AnnouncementService ;
@@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit {
89
123
@Inject ( DI . renoteMutingsRepository )
90
124
private renoteMutingsRepository : RenoteMutingsRepository ,
91
125
92
- @Inject ( DI . driveFilesRepository )
93
- private driveFilesRepository : DriveFilesRepository ,
94
-
95
126
@Inject ( DI . noteUnreadsRepository )
96
127
private noteUnreadsRepository : NoteUnreadsRepository ,
97
128
@@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit {
101
132
@Inject ( DI . userProfilesRepository )
102
133
private userProfilesRepository : UserProfilesRepository ,
103
134
104
- @Inject ( DI . announcementReadsRepository )
105
- private announcementReadsRepository : AnnouncementReadsRepository ,
106
-
107
- @Inject ( DI . announcementsRepository )
108
- private announcementsRepository : AnnouncementsRepository ,
109
-
110
135
@Inject ( DI . userMemosRepository )
111
136
private userMemosRepository : UserMemoRepository ,
112
137
) {
@@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit {
115
140
onModuleInit ( ) {
116
141
this . apPersonService = this . moduleRef . get ( 'ApPersonService' ) ;
117
142
this . noteEntityService = this . moduleRef . get ( 'NoteEntityService' ) ;
118
- this . driveFileEntityService = this . moduleRef . get ( 'DriveFileEntityService' ) ;
119
143
this . pageEntityService = this . moduleRef . get ( 'PageEntityService' ) ;
120
144
this . customEmojiService = this . moduleRef . get ( 'CustomEmojiService' ) ;
121
145
this . announcementService = this . moduleRef . get ( 'AnnouncementService' ) ;
@@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit {
138
162
public isRemoteUser = isRemoteUser ;
139
163
140
164
@bindThis
141
- public async getRelation ( me : MiUser [ 'id' ] , target : MiUser [ 'id' ] ) {
165
+ public async getRelation ( me : MiUser [ 'id' ] , target : MiUser [ 'id' ] ) : Promise < UserRelation > {
142
166
const [
143
167
following ,
144
168
isFollowed ,
@@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit {
211
235
} ;
212
236
}
213
237
238
+ @bindThis
239
+ public async getRelations ( me : MiUser [ 'id' ] , targets : MiUser [ 'id' ] [ ] ) : Promise < Map < MiUser [ 'id' ] , UserRelation > > {
240
+ const [
241
+ followers ,
242
+ followees ,
243
+ followersRequests ,
244
+ followeesRequests ,
245
+ blockers ,
246
+ blockees ,
247
+ muters ,
248
+ renoteMuters ,
249
+ ] = await Promise . all ( [
250
+ this . followingsRepository . findBy ( { followerId : me } )
251
+ . then ( f => new Map ( f . map ( it => [ it . followeeId , it ] ) ) ) ,
252
+ this . followingsRepository . findBy ( { followeeId : me } )
253
+ . then ( it => it . map ( it => it . followerId ) ) ,
254
+ this . followRequestsRepository . findBy ( { followerId : me } )
255
+ . then ( it => it . map ( it => it . followeeId ) ) ,
256
+ this . followRequestsRepository . findBy ( { followeeId : me } )
257
+ . then ( it => it . map ( it => it . followerId ) ) ,
258
+ this . blockingsRepository . findBy ( { blockerId : me } )
259
+ . then ( it => it . map ( it => it . blockeeId ) ) ,
260
+ this . blockingsRepository . findBy ( { blockeeId : me } )
261
+ . then ( it => it . map ( it => it . blockerId ) ) ,
262
+ this . mutingsRepository . findBy ( { muterId : me } )
263
+ . then ( it => it . map ( it => it . muteeId ) ) ,
264
+ this . renoteMutingsRepository . findBy ( { muterId : me } )
265
+ . then ( it => it . map ( it => it . muteeId ) ) ,
266
+ ] ) ;
267
+
268
+ return new Map (
269
+ targets . map ( target => {
270
+ const following = followers . get ( target ) ?? null ;
271
+
272
+ return [
273
+ target ,
274
+ {
275
+ id : target ,
276
+ following : following ,
277
+ isFollowing : following != null ,
278
+ isFollowed : followees . includes ( target ) ,
279
+ hasPendingFollowRequestFromYou : followersRequests . includes ( target ) ,
280
+ hasPendingFollowRequestToYou : followeesRequests . includes ( target ) ,
281
+ isBlocking : blockers . includes ( target ) ,
282
+ isBlocked : blockees . includes ( target ) ,
283
+ isMuted : muters . includes ( target ) ,
284
+ isRenoteMuted : renoteMuters . includes ( target ) ,
285
+ } ,
286
+ ] ;
287
+ } ) ,
288
+ ) ;
289
+ }
290
+
214
291
@bindThis
215
292
public async getHasUnreadAntenna ( userId : MiUser [ 'id' ] ) : Promise < boolean > {
216
293
/*
@@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit {
303
380
schema ?: S ,
304
381
includeSecrets ?: boolean ,
305
382
userProfile ?: MiUserProfile ,
383
+ userRelations ?: Map < MiUser [ 'id' ] , UserRelation > ,
384
+ userMemos ?: Map < MiUser [ 'id' ] , string | null > ,
385
+ pinNotes ?: Map < MiUser [ 'id' ] , MiUserNotePining [ ] > ,
306
386
} ,
307
387
) : Promise < Packed < S > > {
308
388
const opts = Object . assign ( {
@@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit {
317
397
const isMe = meId === user . id ;
318
398
const iAmModerator = me ? await this . roleService . isModerator ( me as MiUser ) : false ;
319
399
320
- const relation = meId && ! isMe && isDetailed ? await this . getRelation ( meId , user . id ) : null ;
321
- const pins = isDetailed ? await this . userNotePiningsRepository . createQueryBuilder ( 'pin' )
322
- . where ( 'pin.userId = :userId' , { userId : user . id } )
323
- . innerJoinAndSelect ( 'pin.note' , 'note' )
324
- . orderBy ( 'pin.id' , 'DESC' )
325
- . getMany ( ) : [ ] ;
326
- const profile = isDetailed ? ( opts . userProfile ?? await this . userProfilesRepository . findOneByOrFail ( { userId : user . id } ) ) : null ;
400
+ const profile = isDetailed
401
+ ? ( opts . userProfile ?? await this . userProfilesRepository . findOneByOrFail ( { userId : user . id } ) )
402
+ : null ;
403
+
404
+ let relation : UserRelation | null = null ;
405
+ if ( meId && ! isMe && isDetailed ) {
406
+ if ( opts . userRelations ) {
407
+ relation = opts . userRelations . get ( user . id ) ?? null ;
408
+ } else {
409
+ relation = await this . getRelation ( meId , user . id ) ;
410
+ }
411
+ }
412
+
413
+ let memo : string | null = null ;
414
+ if ( isDetailed && meId ) {
415
+ if ( opts . userMemos ) {
416
+ memo = opts . userMemos . get ( user . id ) ?? null ;
417
+ } else {
418
+ memo = await this . userMemosRepository . findOneBy ( { userId : meId , targetUserId : user . id } )
419
+ . then ( row => row ?. memo ?? null ) ;
420
+ }
421
+ }
422
+
423
+ let pins : MiUserNotePining [ ] = [ ] ;
424
+ if ( isDetailed ) {
425
+ if ( opts . pinNotes ) {
426
+ pins = opts . pinNotes . get ( user . id ) ?? [ ] ;
427
+ } else {
428
+ pins = await this . userNotePiningsRepository . createQueryBuilder ( 'pin' )
429
+ . where ( 'pin.userId = :userId' , { userId : user . id } )
430
+ . innerJoinAndSelect ( 'pin.note' , 'note' )
431
+ . orderBy ( 'pin.id' , 'DESC' )
432
+ . getMany ( ) ;
433
+ }
434
+ }
327
435
328
436
const followingCount = profile == null ? null :
329
437
( profile . followingVisibility === 'public' ) || isMe ? user . followingCount :
@@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit {
416
524
twoFactorEnabled : profile ! . twoFactorEnabled ,
417
525
usePasswordLessLogin : profile ! . usePasswordLessLogin ,
418
526
securityKeys : profile ! . twoFactorEnabled
419
- ? this . userSecurityKeysRepository . countBy ( {
420
- userId : user . id ,
421
- } ) . then ( result => result >= 1 )
527
+ ? this . userSecurityKeysRepository . countBy ( { userId : user . id } ) . then ( result => result >= 1 )
422
528
: false ,
423
529
roles : this . roleService . getUserRoles ( user . id ) . then ( roles => roles . filter ( role => role . isPublic ) . sort ( ( a , b ) => b . displayOrder - a . displayOrder ) . map ( role => ( {
424
530
id : role . id ,
@@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit {
430
536
isAdministrator : role . isAdministrator ,
431
537
displayOrder : role . displayOrder ,
432
538
} ) ) ) ,
433
- memo : meId == null ? null : await this . userMemosRepository . findOneBy ( {
434
- userId : meId ,
435
- targetUserId : user . id ,
436
- } ) . then ( row => row ?. memo ?? null ) ,
539
+ memo : memo ,
437
540
moderationNote : iAmModerator ? ( profile ! . moderationNote ?? '' ) : undefined ,
438
541
} : { } ) ,
439
542
@@ -514,14 +617,78 @@ export class UserEntityService implements OnModuleInit {
514
617
return await awaitAll ( packed ) ;
515
618
}
516
619
517
- public packMany < S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite' > (
620
+ public async packMany < S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite' > (
518
621
users : ( MiUser [ 'id' ] | MiUser ) [ ] ,
519
622
me ?: { id : MiUser [ 'id' ] } | null | undefined ,
520
623
options ?: {
521
624
schema ?: S ,
522
625
includeSecrets ?: boolean ,
523
626
} ,
524
627
) : Promise < Packed < S > [ ] > {
525
- return Promise . all ( users . map ( u => this . pack ( u , me , options ) ) ) ;
628
+ // -- IDのみの要素を補完して完全なエンティティ一覧を作る
629
+
630
+ const _users = users . filter ( ( user ) : user is MiUser => typeof user !== 'string' ) ;
631
+ if ( _users . length !== users . length ) {
632
+ _users . push (
633
+ ...await this . usersRepository . findBy ( {
634
+ id : In ( users . filter ( ( user ) : user is string => typeof user === 'string' ) ) ,
635
+ } ) ,
636
+ ) ;
637
+ }
638
+ const _userIds = _users . map ( u => u . id ) ;
639
+
640
+ // -- 特に前提条件のない値群を取得
641
+
642
+ const profilesMap = await this . userProfilesRepository . findBy ( { userId : In ( _userIds ) } )
643
+ . then ( profiles => new Map ( profiles . map ( p => [ p . userId , p ] ) ) ) ;
644
+
645
+ // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
646
+
647
+ let userRelations : Map < MiUser [ 'id' ] , UserRelation > = new Map ( ) ;
648
+ let userMemos : Map < MiUser [ 'id' ] , string | null > = new Map ( ) ;
649
+ let pinNotes : Map < MiUser [ 'id' ] , MiUserNotePining [ ] > = new Map ( ) ;
650
+
651
+ if ( options ?. schema !== 'UserLite' ) {
652
+ const meId = me ? me . id : null ;
653
+ if ( meId ) {
654
+ userMemos = await this . userMemosRepository . findBy ( { userId : meId } )
655
+ . then ( memos => new Map ( memos . map ( memo => [ memo . targetUserId , memo . memo ] ) ) ) ;
656
+
657
+ if ( _userIds . length > 0 ) {
658
+ userRelations = await this . getRelations ( meId , _userIds ) ;
659
+ pinNotes = await this . userNotePiningsRepository . createQueryBuilder ( 'pin' )
660
+ . where ( 'pin.userId IN (:...userIds)' , { userIds : _userIds } )
661
+ . innerJoinAndSelect ( 'pin.note' , 'note' )
662
+ . getMany ( )
663
+ . then ( pinsNotes => {
664
+ const map = new Map < MiUser [ 'id' ] , MiUserNotePining [ ] > ( ) ;
665
+ for ( const note of pinsNotes ) {
666
+ const notes = map . get ( note . userId ) ?? [ ] ;
667
+ notes . push ( note ) ;
668
+ map . set ( note . userId , notes ) ;
669
+ }
670
+ for ( const [ , notes ] of map . entries ( ) ) {
671
+ // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく
672
+ notes . sort ( ( a , b ) => b . id . localeCompare ( a . id ) ) ;
673
+ }
674
+ return map ;
675
+ } ) ;
676
+ }
677
+ }
678
+ }
679
+
680
+ return Promise . all (
681
+ _users . map ( u => this . pack (
682
+ u ,
683
+ me ,
684
+ {
685
+ ...options ,
686
+ userProfile : profilesMap . get ( u . id ) ,
687
+ userRelations : userRelations ,
688
+ userMemos : userMemos ,
689
+ pinNotes : pinNotes ,
690
+ } ,
691
+ ) ) ,
692
+ ) ;
526
693
}
527
694
}
0 commit comments