enhance: account migration (#10592)

* copy block and mute then create follow and unfollow jobs

* copy block and mute and update lists when detecting an account has moved

* no need to care promise orders

* refactor updating actor and target

* automatically accept if a locked account had accepted an old account

* fix exception format

* prevent the old account from calling some endpoints

* do not unfollow when moving

* adjust following and follower counts

* check movedToUri when receiving a follow request

* skip if no need to adjust

* Revert "disable account migration"

This reverts commit 2321214c98.

* fix translation specifier

* fix checking alsoKnownAs and uri

* fix updating account

* fix refollowing locked account

* decrease followersCount if followed by the old account

* adjust following and followers counts when unfollowing

* fix copying mutings

* prohibit moved account from moving again

* fix move service

* allow app creation after moving

* fix lint

* remove unnecessary field

* fix cache update

* add e2e test

* add e2e test of accepting the new account automatically

* force follow if any error happens

* remove unnecessary joins

* use Array.map instead of for const of

* ユーザーリストの移行は追加のみを行う

* nanka iroiro

* fix misskey-js?

* ✌️

* 移行を行ったアカウントからのフォローリクエストの自動許可を調整

* newUriを外に出す

* newUriを外に出す2

* clean up

* fix newUri

* prevent moving if the destination account has already moved

* set alsoKnownAs via /i/update

* fix database initialization

* add return type

* prohibit updating alsoKnownAs after moving

* skip to add to alsoKnownAs if toUrl is known

* skip adding to the list if it already has

* use Acct.parse instead

* rename error code

* 🎨

* 制限を5から10に緩和

* movedTo(Uri), alsoKnownAsはユーザーidを返すように

* test api res

* fix

* 元アカウントはミュートし続ける

* 🎨

* unfollow

* fix

* getUserUriをUserEntityServiceに

* ?

* job!

* 🎨

* instance => server

* accountMovedShort, forbiddenBecauseYouAreMigrated

* accountMovedShort

* fix test

* import, pin禁止

* 実績を凍結する

* clean up

* ✌️

* change message

* ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに

* Revert "ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに"

This reverts commit 3bd7be35d8aa455cb01ae58f8172a71a50485db1.

* validateAlsoKnownAs

* 移行後2時間以内はインポート可能なファイルサイズを拡大

* clean up

* どうせactorをupdatePersonで更新するならupdatePersonしか移行処理を発行しないことにする

* handle error?

* リモートからの移行処理の条件を是正

* log, port

* fix

* fix

* enhance(dev): non-production環境でhttpサーバー間でもユーザー、ノートの連合が可能なように

* refactor (use checkHttps)

* MISSKEY_WEBFINGER_USE_HTTP

* Environment Variable readme

* NEVER USE IN PRODUCTION

* fix punyHost

* fix indent

* fix

* experimental

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
Namekuji 2023-04-29 11:09:29 -04:00 committed by GitHub
parent 149ddebf16
commit d28866f71a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 1558 additions and 470 deletions

View file

@ -1,6 +1,6 @@
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@ -22,6 +22,8 @@ import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import Logger from '../logger.js';
import { IsNull } from 'typeorm';
import { AccountMoveService } from '@/core/AccountMoveService.js';
const logger = new Logger('following/create');
@ -73,6 +75,7 @@ export class UserFollowingService implements OnModuleInit {
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@ -87,7 +90,7 @@ export class UserFollowingService implements OnModuleInit {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }),
]);
]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
// check blocking
const [blocking, blocked] = await Promise.all([
@ -137,6 +140,20 @@ export class UserFollowingService implements OnModuleInit {
if (followed) autoAccept = true;
}
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
if (followee.isLocked && !autoAccept) {
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
follower,
(oldSrc, newSrc) => this.followingsRepository.exist({
where: {
followeeId: followee.id,
followerId: newSrc.id,
},
}),
true,
));
}
if (!autoAccept) {
await this.createFollowRequest(follower, followee, requestId);
return;
@ -210,32 +227,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) {
@ -283,12 +308,18 @@ export class UserFollowingService implements OnModuleInit {
},
silent = false,
): Promise<void> {
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 || !following.follower || !following.followee) {
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return;
}
@ -297,7 +328,7 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id);
this.decrementFollowing(follower, followee);
this.decrementFollowing(following.follower, following.followee);
// Publish unfollow event
if (!silent && this.userEntityService.isLocalUser(follower)) {
@ -316,50 +347,87 @@ export class UserFollowingService implements OnModuleInit {
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
}
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
// local user has null host
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as PartialRemoteUser, followee as PartialLocalUser), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@bindThis
private async decrementFollowing(
follower: { id: User['id']; host: User['host']; },
followee: { id: User['id']; host: User['host']; },
follower: User,
followee: User,
): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
//#region Decrement following / followers counts
await Promise.all([
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
]);
//#endregion
// Neither followee nor follower has moved.
if (!follower.movedToUri && !followee.movedToUri) {
//#region Decrement following / followers counts
await Promise.all([
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
this.usersRepository.decrement({ 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.decrement({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
//#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.fetch(follower.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowing(i.host, false);
}
});
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.fetch(followee.host).then(async i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
this.instanceChart.updateFollowers(i.host, false);
}
});
}
//#endregion
this.perUserFollowingChart.update(follower, followee, false);
} else {
// Adjust following/followers counts
for (const user of [follower, followee]) {
if (user.movedToUri) continue; // No need to update if the user has already moved.
const nonMovedFollowees = await this.followingsRepository.count({
relations: {
followee: true,
},
where: {
followerId: user.id,
followee: {
movedToUri: IsNull(),
}
}
});
const nonMovedFollowers = await this.followingsRepository.count({
relations: {
follower: true,
},
where: {
followeeId: user.id,
follower: {
movedToUri: IsNull(),
}
}
});
await this.usersRepository.update(
{ id: user.id },
{ followingCount: nonMovedFollowees, followersCount: nonMovedFollowers },
);
}
// TODO: adjust charts
}
//#endregion
this.perUserFollowingChart.update(follower, followee, false);
}
@bindThis
@ -415,7 +483,7 @@ export class UserFollowingService implements OnModuleInit {
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
this.queueService.deliver(follower, content, followee.inbox, false);
}
}
@ -430,7 +498,7 @@ export class UserFollowingService implements OnModuleInit {
},
): Promise<void> {
if (this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser | PartialRemoteUser, followee as PartialRemoteUser), follower));
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
this.queueService.deliver(follower, content, followee.inbox, false);
@ -475,7 +543,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as PartialLocalUser, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
@ -562,15 +630,22 @@ export class UserFollowingService implements OnModuleInit {
*/
@bindThis
private async removeFollow(followee: Both, follower: Both): Promise<void> {
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;
if (!following || !following.followee || !following.follower) return;
await this.followingsRepository.delete(following.id);
this.decrementFollowing(follower, followee);
this.decrementFollowing(following.follower, following.followee);
}
/**