2023-04-08 01:16:26 -04:00
import { Inject , Injectable } from '@nestjs/common' ;
2023-04-14 09:15:13 -04:00
import { IsNull , In } from 'typeorm' ;
2023-04-08 01:16:26 -04:00
import { bindThis } from '@/decorators.js' ;
import { DI } from '@/di-symbols.js' ;
2023-04-11 20:47:54 -04:00
import type { Config } from '@/config.js' ;
2023-04-08 01:16:26 -04:00
import type { LocalUser } from '@/models/entities/User.js' ;
2023-04-14 09:15:13 -04:00
import type { BlockingsRepository , FollowingsRepository , InstancesRepository , Muting , MutingsRepository , UserListJoiningsRepository , UsersRepository } from '@/models/index.js' ;
2023-04-13 10:43:29 -04:00
import type { RelationshipJobData , ThinUser } from '@/queue/types.js' ;
2023-04-14 09:15:13 -04:00
import type { User } from '@/models/entities/User.js' ;
2023-04-08 01:16:26 -04:00
2023-04-11 20:47:54 -04:00
import { AccountUpdateService } from '@/core/AccountUpdateService.js' ;
2023-04-08 01:16:26 -04:00
import { GlobalEventService } from '@/core/GlobalEventService.js' ;
2023-04-11 20:47:54 -04:00
import { QueueService } from '@/core/QueueService.js' ;
import { RelayService } from '@/core/RelayService.js' ;
2023-04-08 01:16:26 -04:00
import { UserFollowingService } from '@/core/UserFollowingService.js' ;
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js' ;
import { ApRendererService } from '@/core/activitypub/ApRendererService.js' ;
import { UserEntityService } from '@/core/entities/UserEntityService.js' ;
2023-04-11 20:47:54 -04:00
import { IdService } from '@/core/IdService.js' ;
2023-04-14 09:15:13 -04:00
import { CacheService } from '@/core/CacheService.js' ;
2023-04-13 10:43:29 -04:00
import { ProxyAccountService } from '@/core/ProxyAccountService.js' ;
2023-04-14 09:15:13 -04:00
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js' ;
import { MetaService } from '@/core/MetaService.js' ;
import InstanceChart from '@/core/chart/charts/instance.js' ;
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js' ;
2023-04-08 01:16:26 -04:00
@Injectable ( )
export class AccountMoveService {
constructor (
2023-04-11 20:47:54 -04:00
@Inject ( DI . config )
private config : Config ,
2023-04-08 01:16:26 -04:00
@Inject ( DI . usersRepository )
private usersRepository : UsersRepository ,
@Inject ( DI . followingsRepository )
private followingsRepository : FollowingsRepository ,
2023-04-11 20:47:54 -04:00
@Inject ( DI . blockingsRepository )
private blockingsRepository : BlockingsRepository ,
@Inject ( DI . mutingsRepository )
private mutingsRepository : MutingsRepository ,
2023-04-13 10:43:29 -04:00
@Inject ( DI . userListJoiningsRepository )
private userListJoiningsRepository : UserListJoiningsRepository ,
2023-04-14 09:15:13 -04:00
@Inject ( DI . instancesRepository )
private instancesRepository : InstancesRepository ,
2023-04-11 20:47:54 -04:00
private idService : IdService ,
2023-04-08 01:16:26 -04:00
private userEntityService : UserEntityService ,
private apRendererService : ApRendererService ,
private apDeliverManagerService : ApDeliverManagerService ,
private globalEventService : GlobalEventService ,
private userFollowingService : UserFollowingService ,
private accountUpdateService : AccountUpdateService ,
2023-04-13 10:43:29 -04:00
private proxyAccountService : ProxyAccountService ,
2023-04-14 09:15:13 -04:00
private perUserFollowingChart : PerUserFollowingChart ,
private federatedInstanceService : FederatedInstanceService ,
private instanceChart : InstanceChart ,
private metaService : MetaService ,
2023-04-08 01:16:26 -04:00
private relayService : RelayService ,
2023-04-11 20:47:54 -04:00
private cacheService : CacheService ,
private queueService : QueueService ,
2023-04-08 01:16:26 -04:00
) {
}
/ * *
2023-04-11 20:47:54 -04:00
* Move a local account to a new account .
2023-04-08 01:16:26 -04:00
*
* After delivering Move activity , its local followers unfollow the old account and then follow the new one .
* /
@bindThis
2023-04-11 20:47:54 -04:00
public async moveFromLocal ( src : LocalUser , dst : User ) : Promise < unknown > {
2023-04-15 00:47:01 -04:00
const dstUri = this . getUserUri ( dst ) ;
2023-04-08 01:16:26 -04:00
// add movedToUri to indicate that the user has moved
const update = { } as Partial < User > ;
2023-04-15 00:47:01 -04:00
update . alsoKnownAs = src . alsoKnownAs ? . concat ( [ dstUri ] ) ? ? [ dstUri ] ;
update . movedToUri = dstUri ;
2023-04-08 01:16:26 -04:00
await this . usersRepository . update ( src . id , update ) ;
2023-04-15 01:15:23 -04:00
src = Object . assign ( src , update ) ;
2023-04-08 01:16:26 -04:00
const srcPerson = await this . apRendererService . renderPerson ( src ) ;
const updateAct = this . apRendererService . addContext ( this . apRendererService . renderUpdate ( srcPerson , src ) ) ;
await this . apDeliverManagerService . deliverToFollowers ( src , updateAct ) ;
this . relayService . deliverToRelays ( src , updateAct ) ;
// Deliver Move activity to the followers of the old account
const moveAct = this . apRendererService . addContext ( this . apRendererService . renderMove ( src , dst ) ) ;
await this . apDeliverManagerService . deliverToFollowers ( src , moveAct ) ;
// Publish meUpdated event
const iObj = await this . userEntityService . pack < true , true > ( src . id , src , { detail : true , includeSecrets : true } ) ;
this . globalEventService . publishMainStream ( src . id , 'meUpdated' , iObj ) ;
2023-04-11 20:47:54 -04:00
// Move!
await this . move ( src , dst ) ;
2023-04-08 01:16:26 -04:00
return iObj ;
}
/ * *
* Create an alias of an old remote account .
*
* The user ' s new profile will be published to the followers .
* /
@bindThis
public async createAlias ( me : LocalUser , updates : Partial < User > ) : Promise < unknown > {
await this . usersRepository . update ( me . id , updates ) ;
2023-04-15 01:15:23 -04:00
me = Object . assign ( me , updates ) ;
2023-04-08 01:16:26 -04:00
// Publish meUpdated event
const iObj = await this . userEntityService . pack < true , true > ( me . id , me , {
detail : true ,
includeSecrets : true ,
} ) ;
this . globalEventService . publishMainStream ( me . id , 'meUpdated' , iObj ) ;
if ( me . isLocked === false ) {
await this . userFollowingService . acceptAllFollowRequests ( me ) ;
}
this . accountUpdateService . publishToFollowers ( me . id ) ;
return iObj ;
}
2023-04-11 20:47:54 -04:00
@bindThis
public async move ( src : User , dst : User ) : Promise < void > {
2023-04-13 10:49:17 -04:00
// Copy blockings and mutings, and update lists
await Promise . all ( [
this . copyBlocking ( src , dst ) ,
this . copyMutings ( src , dst ) ,
this . updateLists ( src , dst ) ,
] ) ;
2023-04-13 10:43:29 -04:00
// follow the new account and unfollow the old one
const followings = await this . followingsRepository . find ( {
relations : {
follower : true ,
} ,
where : {
followeeId : src.id ,
followerHost : IsNull ( ) , // follower is local
} ,
} ) ;
const followJobs : RelationshipJobData [ ] = [ ] ;
for ( const following of followings ) {
if ( ! following . follower ) continue ;
followJobs . push ( { from : { id : following.follower.id } , to : { id : dst.id } } ) ;
}
2023-04-14 09:15:13 -04:00
// Decrease following count instead of unfollowing.
await this . adjustFollowingCounts ( followJobs . map ( job = > job . from . id ) , src ) ;
2023-04-13 14:22:20 -04:00
// Should be queued because this can cause a number of follow per one move.
2023-04-13 10:43:29 -04:00
this . queueService . createFollowJob ( followJobs ) ;
}
@bindThis
public async copyBlocking ( src : ThinUser , dst : ThinUser ) : Promise < void > {
2023-04-11 20:47:54 -04:00
// Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving.
// So block the destination account here.
2023-04-13 10:43:29 -04:00
const blockings = await this . blockingsRepository . find ( { // FIXME: might be expensive
2023-04-11 20:47:54 -04:00
relations : {
blocker : true
} ,
where : {
blockeeId : src.id
}
2023-04-13 10:43:29 -04:00
} ) ;
2023-04-11 20:47:54 -04:00
// reblock the destination account
const blockJobs : RelationshipJobData [ ] = [ ] ;
for ( const blocking of blockings ) {
if ( ! blocking . blocker ) continue ;
2023-04-13 10:43:29 -04:00
blockJobs . push ( { from : { id : blocking.blocker.id } , to : { id : dst.id } } ) ;
2023-04-11 20:47:54 -04:00
}
// no need to unblock the old account because it may be still functional
this . queueService . createBlockJob ( blockJobs ) ;
2023-04-13 10:43:29 -04:00
}
2023-04-11 20:47:54 -04:00
2023-04-13 10:43:29 -04:00
@bindThis
public async copyMutings ( src : ThinUser , dst : ThinUser ) : Promise < void > {
2023-04-11 20:47:54 -04:00
// Insert new mutings with the same values except mutee
const mutings = await this . mutingsRepository . findBy ( { muteeId : src.id } ) ;
const newMuting : Partial < Muting > [ ] = [ ] ;
for ( const muting of mutings ) {
newMuting . push ( {
id : this.idService.genId ( ) ,
createdAt : new Date ( ) ,
expiresAt : muting.expiresAt ,
muterId : muting.muterId ,
muteeId : dst.id ,
2023-04-13 10:43:29 -04:00
} ) ;
2023-04-11 20:47:54 -04:00
}
this . mutingsRepository . insert ( mutings ) ; // no need to wait
for ( const mute of mutings ) {
if ( mute . muter ) this . cacheService . userMutingsCache . refresh ( mute . muter . id ) ;
}
// no need to unmute the old account because it may be still functional
2023-04-13 10:43:29 -04:00
}
2023-04-11 20:47:54 -04:00
2023-04-13 10:43:29 -04:00
@bindThis
public async updateLists ( src : ThinUser , dst : User ) : Promise < void > {
2023-04-13 10:49:17 -04:00
// Return if there is no list to be updated.
const exists = await this . userListJoiningsRepository . exist ( {
where : {
userId : src.id ,
} ,
} ) ;
if ( ! exists ) return ;
2023-04-13 10:43:29 -04:00
await this . userListJoiningsRepository . update (
{ userId : src.id } ,
{ userId : dst.id , user : dst }
) ;
// Have the proxy account follow the new account in the same way as UserListService.push
if ( this . userEntityService . isRemoteUser ( dst ) ) {
const proxy = await this . proxyAccountService . fetch ( ) ;
if ( proxy ) {
this . queueService . createFollowJob ( [ { from : { id : proxy.id } , to : { id : dst.id } } ] ) ;
}
2023-04-11 20:47:54 -04:00
}
}
@bindThis
public getUserUri ( user : User ) : string {
return this . userEntityService . isRemoteUser ( user )
? user . uri : ` ${ this . config . url } /users/ ${ user . id } ` ;
}
2023-04-14 09:15:13 -04:00
@bindThis
2023-04-14 10:14:40 -04:00
private async adjustFollowingCounts ( localFollowerIds : string [ ] , oldAccount : User ) : Promise < void > {
if ( localFollowerIds . length === 0 ) return ;
2023-04-14 09:15:13 -04:00
// Set the old account's following and followers counts to 0.
2023-04-14 10:14:40 -04:00
await this . usersRepository . update ( { id : oldAccount.id } , { followersCount : 0 , followingCount : 0 } ) ;
2023-04-14 09:15:13 -04:00
// Decrease following counts of local followers by 1.
await this . usersRepository . decrement ( { id : In ( localFollowerIds ) } , 'followingCount' , 1 ) ;
2023-04-15 02:20:26 -04:00
// Decrease follower counts of local followees by 1.
const oldFollowings = await this . followingsRepository . findBy ( { followerId : oldAccount.id } ) ;
if ( oldFollowings . length > 0 ) {
await this . usersRepository . decrement ( { id : In ( oldFollowings . map ( following = > following . followeeId ) ) } , 'followersCount' , 1 ) ;
}
2023-04-14 09:15:13 -04:00
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
if ( this . userEntityService . isRemoteUser ( oldAccount ) ) {
this . federatedInstanceService . fetch ( oldAccount . host ) . then ( async i = > {
this . instancesRepository . decrement ( { id : i.id } , 'followersCount' , localFollowerIds . length ) ;
if ( ( await this . metaService . fetch ( ) ) . enableChartsForFederatedInstances ) {
this . instanceChart . updateFollowers ( i . host , false ) ;
}
} ) ;
}
// FIXME: expensive?
for ( const followerId of localFollowerIds ) {
this . perUserFollowingChart . update ( { id : followerId , host : null } , oldAccount , false ) ;
}
}
2023-04-08 01:16:26 -04:00
}