2023-04-08 01:16:26 -04:00
import { Inject , Injectable } from '@nestjs/common' ;
import { IsNull } from 'typeorm' ;
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-13 10:43:29 -04:00
import type { BlockingsRepository , FollowingsRepository , Muting , MutingsRepository , UserListJoiningsRepository , UsersRepository } from '@/models/index.js' ;
import type { RelationshipJobData , ThinUser } from '@/queue/types.js' ;
2023-04-11 20:47:54 -04:00
2023-04-08 01:16:26 -04:00
import { User } from '@/models/entities/User.js' ;
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' ;
import { CacheService } from '@/core/CacheService' ;
2023-04-13 10:43:29 -04:00
import { ProxyAccountService } from '@/core/ProxyAccountService.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-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-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-08 01:16:26 -04:00
if ( ! dst . uri ) throw new Error ( 'destination uri is empty' ) ;
// add movedToUri to indicate that the user has moved
const update = { } as Partial < User > ;
update . alsoKnownAs = src . alsoKnownAs ? . concat ( [ dst . uri ] ) ? ? [ dst . uri ] ;
update . movedToUri = dst . uri ;
await this . usersRepository . update ( src . id , update ) ;
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 ) ;
// 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 > {
// Copy blockings:
2023-04-13 10:43:29 -04:00
await this . copyBlocking ( src , dst ) ;
// Copy mutings:
await this . copyMutings ( src , dst ) ;
// Update lists:
await this . updateLists ( src , dst ) ;
// 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 [ ] = [ ] ;
const unfollowJobs : RelationshipJobData [ ] = [ ] ;
for ( const following of followings ) {
if ( ! following . follower ) continue ;
followJobs . push ( { from : { id : following.follower.id } , to : { id : dst.id } } ) ;
unfollowJobs . push ( { from : { id : following.follower.id } , to : { id : src.id } } ) ;
}
// Should be queued because this can cause a number of follow/unfollow per one move.
// No need to care job orders as there should be no overlaps of follow/unfollow target.
this . queueService . createFollowJob ( followJobs ) ;
this . queueService . createUnfollowJob ( unfollowJobs ) ;
}
@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 > {
// Return if there is no list to be updated
const numOfLists = await this . userListJoiningsRepository . countBy ( { userId : src.id } ) ;
if ( numOfLists === 0 ) return ;
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-08 01:16:26 -04:00
}