From fc327f0567c0d058b4d5b48ad34eb3c875a20751 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Fri, 14 Apr 2023 09:15:13 -0400 Subject: [PATCH] adjust following and follower counts --- .../backend/src/core/AccountMoveService.ts | 48 +++++++++- .../backend/src/core/UserFollowingService.ts | 91 ++++++++++++------- 2 files changed, 102 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 4362b40e29..3a9d70a796 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -1,14 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; +import { IsNull, In } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { LocalUser } from '@/models/entities/User.js'; -import type { BlockingsRepository, FollowingsRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; - -import { User } from '@/models/entities/User.js'; +import type { User } from '@/models/entities/User.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -19,8 +18,12 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService'; +import { CacheService } from '@/core/CacheService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; +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'; @Injectable() export class AccountMoveService { @@ -43,6 +46,9 @@ export class AccountMoveService { @Inject(DI.userListJoiningsRepository) private userListJoiningsRepository: UserListJoiningsRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + private idService: IdService, private userEntityService: UserEntityService, private apRendererService: ApRendererService, @@ -51,6 +57,10 @@ export class AccountMoveService { private userFollowingService: UserFollowingService, private accountUpdateService: AccountUpdateService, private proxyAccountService: ProxyAccountService, + private perUserFollowingChart: PerUserFollowingChart, + private federatedInstanceService: FederatedInstanceService, + private instanceChart: InstanceChart, + private metaService: MetaService, private relayService: RelayService, private cacheService: CacheService, private queueService: QueueService, @@ -140,6 +150,10 @@ export class AccountMoveService { if (!following.follower) continue; followJobs.push({ from: { id: following.follower.id }, to: { id: dst.id } }); } + + // Decrease following count instead of unfollowing. + await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src); + // Should be queued because this can cause a number of follow per one move. this.queueService.createFollowJob(followJobs); } @@ -216,4 +230,28 @@ export class AccountMoveService { return this.userEntityService.isRemoteUser(user) ? user.uri : `${this.config.url}/users/${user.id}`; } + + @bindThis + private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User) { + // Set the old account's following and followers counts to 0. + await this.usersRepository.update(oldAccount.id, { followersCount: 0, followingCount: 0 }); + + // Decrease following counts of local followers by 1. + await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1); + + // 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); + } + } } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index f32048bf38..addbca8000 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -230,32 +230,40 @@ export class UserFollowingService implements OnModuleInit { this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); - //#region Increment counts - await Promise.all([ - this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), - this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), + const [followeeUser, followerUser] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: followee.id }), + this.usersRepository.findOneByOrFail({ id: follower.id }), ]); - //#endregion - //#region Update instance stats - if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - this.federatedInstanceService.fetch(follower.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowing(i.host, true); - } - }); - } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { - this.federatedInstanceService.fetch(followee.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateFollowers(i.host, true); - } - }); + // Neither followee nor follower has moved. + if (!followeeUser.movedToUri && !followerUser.movedToUri) { + //#region Increment counts + await Promise.all([ + this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), + this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), + ]); + //#endregion + + //#region Update instance stats + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { + this.federatedInstanceService.fetch(follower.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowing(i.host, true); + } + }); + } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { + this.federatedInstanceService.fetch(followee.host).then(async i => { + this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); + if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.instanceChart.updateFollowers(i.host, true); + } + }); + } + //#endregion + + this.perUserFollowingChart.update(follower, followee, true); } - //#endregion - - this.perUserFollowingChart.update(follower, followee, true); // Publish follow event if (this.userEntityService.isLocalUser(follower) && !silent) { @@ -303,12 +311,18 @@ export class UserFollowingService implements OnModuleInit { }, silent = false, ): Promise { - const following = await this.followingsRepository.findOneBy({ - followerId: follower.id, - followeeId: followee.id, + const following = await this.followingsRepository.findOne({ + relations: { + follower: true, + followee: true, + }, + where: { + followerId: follower.id, + followeeId: followee.id, + } }); - if (following == null) { + if (following === null) { logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); return; } @@ -317,7 +331,10 @@ export class UserFollowingService implements OnModuleInit { this.cacheService.userFollowingsCache.refresh(follower.id); - this.decrementFollowing(follower, followee); + // Neither followee nor follower has moved. + if (!following.followee?.movedToUri && !following.follower?.movedToUri) { + this.decrementFollowing(follower, followee); + } // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { @@ -582,15 +599,25 @@ export class UserFollowingService implements OnModuleInit { */ @bindThis private async removeFollow(followee: Both, follower: Both): Promise { - const following = await this.followingsRepository.findOneBy({ - followeeId: followee.id, - followerId: follower.id, + const following = await this.followingsRepository.findOne({ + relations: { + followee: true, + follower: true, + }, + where: { + followeeId: followee.id, + followerId: follower.id, + } }); if (!following) return; await this.followingsRepository.delete(following.id); - this.decrementFollowing(follower, followee); + + // Neither followee nor follower has moved. + if (!following.followee?.movedToUri && !following.follower?.movedToUri) { + this.decrementFollowing(follower, followee); + } } /**