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:
parent
149ddebf16
commit
d28866f71a
94 changed files with 1558 additions and 470 deletions
|
@ -133,16 +133,20 @@ id: 'aid'
|
||||||
#clusterLimit: 1
|
#clusterLimit: 1
|
||||||
|
|
||||||
# Job concurrency per worker
|
# Job concurrency per worker
|
||||||
# deliverJobConcurrency: 128
|
#deliverJobConcurrency: 128
|
||||||
# inboxJobConcurrency: 16
|
#inboxJobConcurrency: 16
|
||||||
|
#relashionshipJobConcurrency: 16
|
||||||
|
# What's relashionshipJob?:
|
||||||
|
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
|
||||||
|
|
||||||
# Job rate limiter
|
# Job rate limiter
|
||||||
# deliverJobPerSec: 128
|
#deliverJobPerSec: 128
|
||||||
# inboxJobPerSec: 16
|
#inboxJobPerSec: 16
|
||||||
|
#relashionshipJobPerSec: 64
|
||||||
|
|
||||||
# Job attempts
|
# Job attempts
|
||||||
# deliverJobMaxAttempts: 12
|
#deliverJobMaxAttempts: 12
|
||||||
# inboxJobMaxAttempts: 8
|
#inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
#outgoingAddressFamily: ipv4
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -6,5 +6,6 @@
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.test.ts": "typescript"
|
"*.test.ts": "typescript"
|
||||||
},
|
},
|
||||||
|
"jest.jestCommandLine": "pnpm run jest",
|
||||||
"jest.autoRun": "off"
|
"jest.autoRun": "off"
|
||||||
}
|
}
|
|
@ -23,6 +23,8 @@
|
||||||
(自分自身に対してもメモを追加できます。)
|
(自分自身に対してもメモを追加できます。)
|
||||||
* ユーザーメニューから追加できます。
|
* ユーザーメニューから追加できます。
|
||||||
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
|
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
|
||||||
|
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||||
|
* 一度引っ越したアカウントは利用に制限がかかります
|
||||||
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
- ロールタイムラインをロールごとに表示するかどうかの選択できるようになりました。
|
||||||
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
* デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。
|
||||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||||
|
|
|
@ -703,6 +703,8 @@ contact: "連絡先"
|
||||||
useSystemFont: "システムのデフォルトのフォントを使う"
|
useSystemFont: "システムのデフォルトのフォントを使う"
|
||||||
clips: "クリップ"
|
clips: "クリップ"
|
||||||
experimentalFeatures: "実験的機能"
|
experimentalFeatures: "実験的機能"
|
||||||
|
experimental: "実験的"
|
||||||
|
ThisIsExperimentalFeature: "これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。"
|
||||||
developer: "開発者"
|
developer: "開発者"
|
||||||
makeExplorable: "アカウントを見つけやすくする"
|
makeExplorable: "アカウントを見つけやすくする"
|
||||||
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
|
makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。"
|
||||||
|
@ -1003,8 +1005,10 @@ noteIdOrUrl: "ノートIDまたはURL"
|
||||||
video: "動画"
|
video: "動画"
|
||||||
videos: "動画"
|
videos: "動画"
|
||||||
dataSaver: "データセーバー"
|
dataSaver: "データセーバー"
|
||||||
accountMigration: "アカウントの引っ越し"
|
accountMigration: "アカウントの移行"
|
||||||
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
accountMoved: "このユーザーは新しいアカウントに移行しました:"
|
||||||
|
accountMovedShort: "このアカウントは移行されています"
|
||||||
|
operationForbidden: "この操作はできません"
|
||||||
forceShowAds: "常に広告を表示する"
|
forceShowAds: "常に広告を表示する"
|
||||||
addMemo: "メモを追加"
|
addMemo: "メモを追加"
|
||||||
editMemo: "メモを編集"
|
editMemo: "メモを編集"
|
||||||
|
@ -1030,13 +1034,20 @@ _serverRules:
|
||||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveTo: "このアカウントを新しいアカウントに引っ越す"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
moveToLabel: "引っ越し先のアカウント:"
|
moveFromSub: "別のアカウントへエイリアスを作成"
|
||||||
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
|
moveFromLabel: "移行元のアカウント #{n}"
|
||||||
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
moveFromDescription: "別のアカウントからこのアカウントに移行したい場合、ここでエイリアスを作成しておく必要があります。\n移行元のアカウントをこのように入力してください: @username@server.example.com\n削除するには、入力欄を空にして保存します(非推奨)。"
|
||||||
moveFromLabel: "引っ越し元のアカウント:"
|
moveTo: "このアカウントを新しいアカウントへ移行"
|
||||||
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
|
moveToLabel: "移行先のアカウント:"
|
||||||
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用できなくなります。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
|
moveCannotBeUndone: "アカウントを移行すると、取り消すことはできません。"
|
||||||
|
moveAccountDescription: "新しいアカウントへ移行します。\n ・フォロワーが新しいアカウントを自動でフォローします\n ・このアカウントからのフォローは全て解除されます\n ・このアカウントではノートの作成などができなくなります\n\nフォロワーの移行は自動ですが、フォローの移行は手動で行う必要があります。移行前にこのアカウントでフォローエクスポートし、移行後すぐに移行先アカウントでインポートを行なってください。\nリスト・ミュート・ブロックについても同様ですので、手動で移行する必要があります。\n\n(この説明はこのサーバー(Misskey v13.12.0以降)の仕様です。Mastodonなどの他のActivityPubソフトウェアでは挙動が異なる場合があります。)"
|
||||||
|
moveAccountHowTo: "アカウントの移行には、まずは移行先のアカウントでこのアカウントに対しエイリアスを作成します。\nエイリアス作成後、移行先のアカウントを次のように入力してください: @username@server.example.com"
|
||||||
|
startMigration: "移行する"
|
||||||
|
migrationConfirm: "本当にこのアカウントを {account} に移行しますか?一度移行すると取り消せず、二度とこのアカウントを元の状態で使用できなくなります。"
|
||||||
|
movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。"
|
||||||
|
postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。"
|
||||||
|
movedTo: "移行先のアカウント:"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
|
|
13
packages/backend/migration/1682190963894-movedAt.js
Normal file
13
packages/backend/migration/1682190963894-movedAt.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class MovedAt1682190963894 {
|
||||||
|
name = 'MovedAt1682190963894'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "movedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."movedAt" IS 'When the user moved to another account'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedAt"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,8 +84,10 @@ export type Source = {
|
||||||
|
|
||||||
deliverJobConcurrency?: number;
|
deliverJobConcurrency?: number;
|
||||||
inboxJobConcurrency?: number;
|
inboxJobConcurrency?: number;
|
||||||
|
relashionshipJobConcurrency?: number;
|
||||||
deliverJobPerSec?: number;
|
deliverJobPerSec?: number;
|
||||||
inboxJobPerSec?: number;
|
inboxJobPerSec?: number;
|
||||||
|
relashionshipJobPerSec?: number;
|
||||||
deliverJobMaxAttempts?: number;
|
deliverJobMaxAttempts?: number;
|
||||||
inboxJobMaxAttempts?: number;
|
inboxJobMaxAttempts?: number;
|
||||||
|
|
||||||
|
|
|
@ -1,55 +1,90 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull, In, MoreThan, Not } from 'typeorm';
|
||||||
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { Config } from '@/config.js';
|
||||||
import { User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import type { FollowingsRepository, 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 type { User } from '@/models/entities/User.js';
|
||||||
|
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { RelayService } from '@/core/RelayService.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()
|
@Injectable()
|
||||||
export class AccountMoveService {
|
export class AccountMoveService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.blockingsRepository)
|
||||||
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutingsRepository)
|
||||||
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userListJoiningsRepository)
|
||||||
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.instancesRepository)
|
||||||
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private idService: IdService,
|
||||||
|
private apPersonService: ApPersonService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private userFollowingService: UserFollowingService,
|
private proxyAccountService: ProxyAccountService,
|
||||||
private accountUpdateService: AccountUpdateService,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
|
private instanceChart: InstanceChart,
|
||||||
|
private metaService: MetaService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a local account to a remote account.
|
* Move a local account to a new account.
|
||||||
*
|
*
|
||||||
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
|
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
|
public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise<unknown> {
|
||||||
// Make sure that the destination is a remote account.
|
const srcUri = this.userEntityService.getUserUri(src);
|
||||||
if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
|
const dstUri = this.userEntityService.getUserUri(dst);
|
||||||
if (!dst.uri) throw new Error('destination uri is empty');
|
|
||||||
|
|
||||||
// add movedToUri to indicate that the user has moved
|
// add movedToUri to indicate that the user has moved
|
||||||
const update = {} as Partial<User>;
|
const update = {} as Partial<LocalUser>;
|
||||||
update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
|
update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri];
|
||||||
update.movedToUri = dst.uri;
|
update.movedToUri = dstUri;
|
||||||
|
update.movedAt = new Date();
|
||||||
await this.usersRepository.update(src.id, update);
|
await this.usersRepository.update(src.id, update);
|
||||||
|
Object.assign(src, update);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
this.cacheService.uriPersonCache.set(srcUri, src);
|
||||||
|
|
||||||
const srcPerson = await this.apRendererService.renderPerson(src);
|
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||||
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||||
|
@ -64,51 +99,249 @@ export class AccountMoveService {
|
||||||
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
|
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
|
||||||
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
|
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
|
||||||
|
|
||||||
// follow the new account and unfollow the old one
|
// Unfollow after 24 hours
|
||||||
const followings = await this.followingsRepository.find({
|
const followings = await this.followingsRepository.findBy({
|
||||||
relations: {
|
followerId: src.id,
|
||||||
follower: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
followeeId: src.id,
|
|
||||||
followerHost: IsNull(), // follower is local
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
for (const following of followings) {
|
this.queueService.createDelayedUnfollowJob(followings.map(following => ({
|
||||||
if (!following.follower) continue;
|
from: { id: src.id },
|
||||||
try {
|
to: { id: following.followeeId },
|
||||||
await this.userFollowingService.follow(following.follower, dst);
|
})), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
|
||||||
await this.userFollowingService.unfollow(following.follower, src);
|
|
||||||
} catch {
|
await this.postMoveProcess(src, dst);
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return iObj;
|
return iObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async postMoveProcess(src: User, dst: User): Promise<void> {
|
||||||
|
// Copy blockings and mutings, and update lists
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.copyBlocking(src, dst),
|
||||||
|
this.copyMutings(src, dst),
|
||||||
|
this.updateLists(src, dst),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
/* skip if any error happens */
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow the new account
|
||||||
|
const proxy = await this.proxyAccountService.fetch();
|
||||||
|
const followings = await this.followingsRepository.findBy({
|
||||||
|
followeeId: src.id,
|
||||||
|
followerHost: IsNull(), // follower is local
|
||||||
|
followerId: proxy ? Not(proxy.id) : undefined,
|
||||||
|
});
|
||||||
|
const followJobs = followings.map(following => ({
|
||||||
|
from: { id: following.followerId },
|
||||||
|
to: { id: dst.id },
|
||||||
|
})) as RelationshipJobData[];
|
||||||
|
|
||||||
|
// Decrease following count instead of unfollowing.
|
||||||
|
try {
|
||||||
|
await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src);
|
||||||
|
} catch {
|
||||||
|
/* skip if any error happens */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be queued because this can cause a number of follow per one move.
|
||||||
|
this.queueService.createFollowJob(followJobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||||
|
// 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.
|
||||||
|
const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id });
|
||||||
|
const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id });
|
||||||
|
const blockerIds = dstBlockings.map(blocking => blocking.blockerId);
|
||||||
|
// reblock the destination account
|
||||||
|
const blockJobs: RelationshipJobData[] = [];
|
||||||
|
for (const blocking of srcBlockings) {
|
||||||
|
if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked
|
||||||
|
blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } });
|
||||||
|
}
|
||||||
|
// no need to unblock the old account because it may be still functional
|
||||||
|
this.queueService.createBlockJob(blockJobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async copyMutings(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||||
|
// Insert new mutings with the same values except mutee
|
||||||
|
const oldMutings = await this.mutingsRepository.findBy([
|
||||||
|
{ muteeId: src.id, expiresAt: IsNull() },
|
||||||
|
{ muteeId: src.id, expiresAt: MoreThan(new Date()) },
|
||||||
|
]);
|
||||||
|
if (oldMutings.length === 0) return;
|
||||||
|
|
||||||
|
// Check if the destination account is already indefinitely muted by the muter
|
||||||
|
const existingMutingsMuterUserIds = await this.mutingsRepository.findBy(
|
||||||
|
{ muteeId: dst.id, expiresAt: IsNull() },
|
||||||
|
).then(mutings => mutings.map(muting => muting.muterId));
|
||||||
|
|
||||||
|
const newMutings: Map<string, { muterId: string; muteeId: string; createdAt: Date; expiresAt: Date | null; }> = new Map();
|
||||||
|
|
||||||
|
// 重複しないようにIDを生成
|
||||||
|
const genId = (): string => {
|
||||||
|
let id: string;
|
||||||
|
do {
|
||||||
|
id = this.idService.genId();
|
||||||
|
} while (newMutings.has(id));
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
for (const muting of oldMutings) {
|
||||||
|
if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely
|
||||||
|
newMutings.set(genId(), {
|
||||||
|
...muting,
|
||||||
|
createdAt: new Date(),
|
||||||
|
muteeId: dst.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayToInsert = Array.from(newMutings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||||
|
await this.mutingsRepository.insert(arrayToInsert);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an alias of an old remote account.
|
* Update lists while moving accounts.
|
||||||
|
* - No removal of the old account from the lists
|
||||||
|
* - Users number limit is not checked
|
||||||
*
|
*
|
||||||
* The user's new profile will be published to the followers.
|
* @param src ThinUser (old account)
|
||||||
|
* @param dst User (new account)
|
||||||
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
|
public async updateLists(src: ThinUser, dst: User): Promise<void> {
|
||||||
await this.usersRepository.update(me.id, updates);
|
// Return if there is no list to be updated.
|
||||||
|
const oldJoinings = await this.userListJoiningsRepository.find({
|
||||||
// Publish meUpdated event
|
where: {
|
||||||
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
|
userId: src.id,
|
||||||
detail: true,
|
},
|
||||||
includeSecrets: true,
|
|
||||||
});
|
});
|
||||||
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
|
if (oldJoinings.length === 0) return;
|
||||||
|
|
||||||
if (me.isLocked === false) {
|
const existingUserListIds = await this.userListJoiningsRepository.find({
|
||||||
await this.userFollowingService.acceptAllFollowRequests(me);
|
where: {
|
||||||
|
userId: dst.id,
|
||||||
|
},
|
||||||
|
}).then(joinings => joinings.map(joining => joining.userListId));
|
||||||
|
|
||||||
|
const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
|
||||||
|
|
||||||
|
// 重複しないようにIDを生成
|
||||||
|
const genId = (): string => {
|
||||||
|
let id: string;
|
||||||
|
do {
|
||||||
|
id = this.idService.genId();
|
||||||
|
} while (newJoinings.has(id));
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
for (const joining of oldJoinings) {
|
||||||
|
if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
|
||||||
|
newJoinings.set(genId(), {
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: dst.id,
|
||||||
|
userListId: joining.userListId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.accountUpdateService.publishToFollowers(me.id);
|
const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
|
||||||
|
await this.userListJoiningsRepository.insert(arrayToInsert);
|
||||||
|
|
||||||
return iObj;
|
// 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 } }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise<void> {
|
||||||
|
if (localFollowerIds.length === 0) return;
|
||||||
|
|
||||||
|
// Set the old account's following and followers counts to 0.
|
||||||
|
await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 });
|
||||||
|
|
||||||
|
// Decrease following counts of local followers by 1.
|
||||||
|
await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
|
||||||
|
*
|
||||||
|
* @param dst movedToUrlを指定するユーザー
|
||||||
|
* @param check
|
||||||
|
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
|
||||||
|
* @returns Promise<LocalUser | RemoteUser | null>
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async validateAlsoKnownAs(
|
||||||
|
dst: LocalUser | RemoteUser,
|
||||||
|
check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise<boolean> = () => true,
|
||||||
|
instant = false,
|
||||||
|
): Promise<LocalUser | RemoteUser | null> {
|
||||||
|
let resultUser: LocalUser | RemoteUser | null = null;
|
||||||
|
|
||||||
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
|
if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||||
|
await this.apPersonService.updatePerson(dst.uri);
|
||||||
|
}
|
||||||
|
dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) return null;
|
||||||
|
|
||||||
|
const dstUri = this.userEntityService.getUserUri(dst);
|
||||||
|
|
||||||
|
for (const srcUri of dst.alsoKnownAs) {
|
||||||
|
try {
|
||||||
|
let src = await this.apPersonService.fetchPerson(srcUri);
|
||||||
|
if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
|
||||||
|
|
||||||
|
if (this.userEntityService.isRemoteUser(dst)) {
|
||||||
|
if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
|
||||||
|
await this.apPersonService.updatePerson(srcUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
src = await this.apPersonService.fetchPerson(srcUri) ?? src;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.movedToUri === dstUri) {
|
||||||
|
if (await check(resultUser, src)) {
|
||||||
|
resultUser = src;
|
||||||
|
}
|
||||||
|
if (instant && resultUser) return resultUser;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* skip if any error happens */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ const $db: Provider = {
|
||||||
|
|
||||||
const $relationship: Provider = {
|
const $relationship: Provider = {
|
||||||
provide: 'queue:relationship',
|
provide: 'queue:relationship',
|
||||||
useFactory: (config: Config) => q(config, 'relationship'),
|
useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -258,6 +258,12 @@ export class QueueService {
|
||||||
return this.relationshipQueue.addBulk(jobs);
|
return this.relationshipQueue.addBulk(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createDelayedUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[], delay: number) {
|
||||||
|
const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel, { delay }));
|
||||||
|
return this.relationshipQueue.addBulk(jobs);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
|
||||||
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
|
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
|
||||||
|
@ -271,7 +277,7 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData): {
|
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
|
||||||
name: string,
|
name: string,
|
||||||
data: RelationshipJobData,
|
data: RelationshipJobData,
|
||||||
opts: Bull.JobOptions,
|
opts: Bull.JobOptions,
|
||||||
|
@ -287,6 +293,7 @@ export class QueueService {
|
||||||
opts: {
|
opts: {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
|
...opts,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import chalk from 'chalk';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository } from '@/models/index.js';
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
@ -33,7 +33,7 @@ export class RemoteUserResolveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveUser(username: string, host: string | null): Promise<User> {
|
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
|
@ -44,7 +44,7 @@ export class RemoteUserResolveService {
|
||||||
} else {
|
} else {
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
});
|
}) as LocalUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
@ -57,7 +57,7 @@ export class RemoteUserResolveService {
|
||||||
} else {
|
} else {
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
});
|
}) as LocalUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
||||||
|
@ -109,7 +109,7 @@ export class RemoteUserResolveService {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
throw new Error('user not found');
|
throw new Error('user not found');
|
||||||
} else {
|
} else {
|
||||||
return u;
|
return u as LocalUser | RemoteUser;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
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 { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.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 { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
|
||||||
|
@ -73,6 +75,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
@ -87,7 +90,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||||
]);
|
]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
|
||||||
|
|
||||||
// check blocking
|
// check blocking
|
||||||
const [blocking, blocked] = await Promise.all([
|
const [blocking, blocked] = await Promise.all([
|
||||||
|
@ -137,6 +140,20 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
if (followed) autoAccept = true;
|
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) {
|
if (!autoAccept) {
|
||||||
await this.createFollowRequest(follower, followee, requestId);
|
await this.createFollowRequest(follower, followee, requestId);
|
||||||
return;
|
return;
|
||||||
|
@ -210,32 +227,40 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
//#region Increment counts
|
const [followeeUser, followerUser] = await Promise.all([
|
||||||
await Promise.all([
|
this.usersRepository.findOneByOrFail({ id: followee.id }),
|
||||||
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
this.usersRepository.findOneByOrFail({ id: follower.id }),
|
||||||
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
|
||||||
]);
|
]);
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Update instance stats
|
// Neither followee nor follower has moved.
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (!followeeUser.movedToUri && !followerUser.movedToUri) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
//#region Increment counts
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
await Promise.all([
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||||||
this.instanceChart.updateFollowing(i.host, true);
|
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||||||
}
|
]);
|
||||||
});
|
//#endregion
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
|
||||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
//#region Update instance stats
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instanceChart.updateFollowers(i.host, true);
|
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
|
// Publish follow event
|
||||||
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
if (this.userEntityService.isLocalUser(follower) && !silent) {
|
||||||
|
@ -283,12 +308,18 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
},
|
},
|
||||||
silent = false,
|
silent = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const following = await this.followingsRepository.findOne({
|
||||||
followerId: follower.id,
|
relations: {
|
||||||
followeeId: followee.id,
|
follower: true,
|
||||||
|
followee: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following == null) {
|
if (following === null || !following.follower || !following.followee) {
|
||||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -297,7 +328,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
this.decrementFollowing(follower, followee);
|
this.decrementFollowing(following.follower, following.followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
// Publish unfollow event
|
||||||
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||||||
|
@ -316,50 +347,87 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
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);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||||
// local user has null host
|
// 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);
|
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async decrementFollowing(
|
private async decrementFollowing(
|
||||||
follower: { id: User['id']; host: User['host']; },
|
follower: User,
|
||||||
followee: { id: User['id']; host: User['host']; },
|
followee: User,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
//#region Decrement following / followers counts
|
// Neither followee nor follower has moved.
|
||||||
await Promise.all([
|
if (!follower.movedToUri && !followee.movedToUri) {
|
||||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
//#region Decrement following / followers counts
|
||||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
await Promise.all([
|
||||||
]);
|
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||||
//#endregion
|
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||||||
|
]);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowing(i.host, false);
|
this.instanceChart.updateFollowing(i.host, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
this.instanceChart.updateFollowers(i.host, false);
|
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
|
@bindThis
|
||||||
|
@ -415,7 +483,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
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);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,7 +498,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.userEntityService.isRemoteUser(followee)) {
|
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に怒られるので
|
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
|
@ -475,7 +543,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
await this.insertFollowingDoc(followee, follower);
|
await this.insertFollowingDoc(followee, follower);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
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);
|
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,15 +630,22 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async removeFollow(followee: Both, follower: Both): Promise<void> {
|
private async removeFollow(followee: Both, follower: Both): Promise<void> {
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const following = await this.followingsRepository.findOne({
|
||||||
followeeId: followee.id,
|
relations: {
|
||||||
followerId: follower.id,
|
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);
|
await this.followingsRepository.delete(following.id);
|
||||||
this.decrementFollowing(follower, followee);
|
|
||||||
|
this.decrementFollowing(following.follower, following.followee);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class UserSuspendService {
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにDelete配信
|
// 知り得る全SharedInboxにDelete配信
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export class UserSuspendService {
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
// 知り得る全SharedInboxにUndo Delete配信
|
// 知り得る全SharedInboxにUndo Delete配信
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
||||||
|
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
import { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApId } from './type.js';
|
import { getApId } from './type.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
|
@ -101,7 +101,7 @@ export class ApDbResolverService {
|
||||||
* AP Person => Misskey User in DB
|
* AP Person => Misskey User in DB
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getUserFromApId(value: string | IObject): Promise<User | null> {
|
public async getUserFromApId(value: string | IObject): Promise<LocalUser | RemoteUser | null> {
|
||||||
const parsed = this.parseUri(value);
|
const parsed = this.parseUri(value);
|
||||||
|
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
|
@ -109,11 +109,11 @@ export class ApDbResolverService {
|
||||||
|
|
||||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
}).then(x => x ?? undefined)) ?? null;
|
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
}));
|
})) as RemoteUser | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, IsNull } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
@ -13,13 +13,15 @@ import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
|
@ -76,6 +78,8 @@ export class ApInboxService {
|
||||||
private apNoteService: ApNoteService,
|
private apNoteService: ApNoteService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
private apQuestionService: ApQuestionService,
|
private apQuestionService: ApQuestionService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
|
private cacheService: CacheService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
|
@ -140,7 +144,7 @@ export class ApInboxService {
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
await this.flag(actor, activity);
|
await this.flag(actor, activity);
|
||||||
} else if (isMove(activity)) {
|
} else if (isMove(activity)) {
|
||||||
//await this.move(actor, activity);
|
await this.move(actor, activity);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||||
}
|
}
|
||||||
|
@ -158,6 +162,7 @@ export class ApInboxService {
|
||||||
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't queue because the sender may attempt again when timeout
|
||||||
await this.userFollowingService.follow(actor, followee, activity.id);
|
await this.userFollowingService.follow(actor, followee, activity.id);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
@ -596,6 +601,7 @@ export class ApInboxService {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// don't queue because the sender may attempt again when timeout
|
||||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
if (isFollow(object)) return await this.undoFollow(actor, object);
|
||||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
if (isBlock(object)) return await this.undoBlock(actor, object);
|
||||||
if (isLike(object)) return await this.undoLike(actor, object);
|
if (isLike(object)) return await this.undoLike(actor, object);
|
||||||
|
@ -736,53 +742,7 @@ export class ApInboxService {
|
||||||
// fetch the new and old accounts
|
// fetch the new and old accounts
|
||||||
const targetUri = getApHrefNullable(activity.target);
|
const targetUri = getApHrefNullable(activity.target);
|
||||||
if (!targetUri) return 'skip: invalid activity target';
|
if (!targetUri) return 'skip: invalid activity target';
|
||||||
let new_acc = await this.apPersonService.resolvePerson(targetUri);
|
|
||||||
let old_acc = await this.apPersonService.resolvePerson(actor.uri);
|
|
||||||
|
|
||||||
// update them if they're remote
|
return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do';
|
||||||
if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
|
|
||||||
if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
|
|
||||||
|
|
||||||
// retrieve updated users
|
|
||||||
new_acc = await this.apPersonService.resolvePerson(targetUri);
|
|
||||||
old_acc = await this.apPersonService.resolvePerson(actor.uri);
|
|
||||||
|
|
||||||
// check if alsoKnownAs of the new account is valid
|
|
||||||
let isValidMove = true;
|
|
||||||
if (old_acc.uri) {
|
|
||||||
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
|
|
||||||
isValidMove = false;
|
|
||||||
}
|
|
||||||
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
|
|
||||||
isValidMove = false;
|
|
||||||
}
|
|
||||||
if (!isValidMove) {
|
|
||||||
return 'skip: accounts invalid';
|
|
||||||
}
|
|
||||||
|
|
||||||
// add target uri to movedToUri in order to indicate that the user has moved
|
|
||||||
await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
|
|
||||||
|
|
||||||
// follow the new account and unfollow the old one
|
|
||||||
const followings = await this.followingsRepository.find({
|
|
||||||
relations: {
|
|
||||||
follower: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
followeeId: old_acc.id,
|
|
||||||
followerHost: IsNull(), // follower is local
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const following of followings) {
|
|
||||||
if (!following.follower) continue;
|
|
||||||
try {
|
|
||||||
await this.userFollowingService.follow(following.follower, new_acc);
|
|
||||||
await this.userFollowingService.unfollow(following.follower, old_acc);
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'ok';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { PartialLocalUser, LocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js';
|
import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js';
|
||||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import type { Relay } from '@/models/entities/Relay.js';
|
import type { Relay } from '@/models/entities/Relay.js';
|
||||||
|
@ -66,7 +66,7 @@ export class ApRendererService {
|
||||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
||||||
return {
|
return {
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ export class ApRendererService {
|
||||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
||||||
return {
|
return {
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
@ -83,7 +83,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
public renderAnnounce(object: any, note: Note): IAnnounce {
|
||||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||||
|
|
||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
let cc: string[] = [];
|
let cc: string[] = [];
|
||||||
|
@ -103,7 +103,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: `${this.config.url}/users/${note.userId}`,
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
|
@ -126,7 +126,7 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
id: `${this.config.url}/blocks/${block.id}`,
|
id: `${this.config.url}/blocks/${block.id}`,
|
||||||
actor: `${this.config.url}/users/${block.blockerId}`,
|
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
||||||
object: block.blockee.uri,
|
object: block.blockee.uri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ export class ApRendererService {
|
||||||
public renderCreate(object: IObject, note: Note): ICreate {
|
public renderCreate(object: IObject, note: Note): ICreate {
|
||||||
const activity = {
|
const activity = {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: `${this.config.url}/users/${note.userId}`,
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
object,
|
object,
|
||||||
|
@ -151,7 +151,7 @@ export class ApRendererService {
|
||||||
public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete {
|
public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete {
|
||||||
return {
|
return {
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
@ -188,7 +188,7 @@ export class ApRendererService {
|
||||||
public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag {
|
public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag {
|
||||||
return {
|
return {
|
||||||
type: 'Flag',
|
type: 'Flag',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
content,
|
content,
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
@ -199,7 +199,7 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: `${this.config.url}/users/${relayActor.id}`,
|
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
||||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -210,21 +210,21 @@ export class ApRendererService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderFollowUser(id: User['id']) {
|
public async renderFollowUser(id: User['id']) {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: id });
|
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
||||||
return this.userEntityService.isLocalUser(user) ? `${this.config.url}/users/${user.id}` : user.uri;
|
return this.userEntityService.getUserUri(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderFollow(
|
public renderFollow(
|
||||||
follower: { id: User['id']; host: User['host']; uri: User['host'] },
|
follower: PartialLocalUser | PartialRemoteUser,
|
||||||
followee: { id: User['id']; host: User['host']; uri: User['host'] },
|
followee: PartialLocalUser | PartialRemoteUser,
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
): IFollow {
|
): IFollow {
|
||||||
return {
|
return {
|
||||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!,
|
actor: this.userEntityService.getUserUri(follower)!,
|
||||||
object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!,
|
object: this.userEntityService.getUserUri(followee)!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
||||||
type: 'Key',
|
type: 'Key',
|
||||||
owner: `${this.config.url}/users/${user.id}`,
|
owner: this.userEntityService.genLocalUserUri(user.id),
|
||||||
publicKeyPem: createPublicKey(key.publicKey).export({
|
publicKeyPem: createPublicKey(key.publicKey).export({
|
||||||
type: 'spki',
|
type: 'spki',
|
||||||
format: 'pem',
|
format: 'pem',
|
||||||
|
@ -284,21 +284,21 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderMention(mention: User): IApMention {
|
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
||||||
return {
|
return {
|
||||||
type: 'Mention',
|
type: 'Mention',
|
||||||
href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`,
|
href: this.userEntityService.getUserUri(mention)!,
|
||||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderMove(
|
public renderMove(
|
||||||
src: { id: User['id']; host: User['host']; uri: User['host'] },
|
src: PartialLocalUser | PartialRemoteUser,
|
||||||
dst: { id: User['id']; host: User['host']; uri: User['host'] },
|
dst: PartialLocalUser | PartialRemoteUser,
|
||||||
): IMove {
|
): IMove {
|
||||||
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
|
const actor = this.userEntityService.getUserUri(src)!;
|
||||||
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
|
const target = this.userEntityService.getUserUri(dst)!;
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||||
actor,
|
actor,
|
||||||
|
@ -351,7 +351,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||||
|
|
||||||
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||||
|
|
||||||
|
@ -376,7 +376,7 @@ export class ApRendererService {
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
|
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
||||||
|
|
||||||
const files = await getPromisedFiles(note.fileIds);
|
const files = await getPromisedFiles(note.fileIds);
|
||||||
|
|
||||||
|
@ -450,7 +450,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPerson(user: LocalUser) {
|
public async renderPerson(user: LocalUser) {
|
||||||
const id = `${this.config.url}/users/${user.id}`;
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
const isSystem = !!user.username.match(/\./);
|
const isSystem = !!user.username.match(/\./);
|
||||||
|
|
||||||
const [avatar, banner, profile] = await Promise.all([
|
const [avatar, banner, profile] = await Promise.all([
|
||||||
|
@ -538,7 +538,7 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
id: `${this.config.url}/questions/${note.id}`,
|
id: `${this.config.url}/questions/${note.id}`,
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
content: note.text ?? '',
|
content: note.text ?? '',
|
||||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||||
name: text,
|
name: text,
|
||||||
|
@ -555,7 +555,7 @@ export class ApRendererService {
|
||||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
||||||
return {
|
return {
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -564,7 +564,7 @@ export class ApRendererService {
|
||||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
||||||
return {
|
return {
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
@ -585,7 +585,7 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
...(id ? { id } : {}),
|
...(id ? { id } : {}),
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
@ -595,7 +595,7 @@ export class ApRendererService {
|
||||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
object,
|
object,
|
||||||
|
@ -607,14 +607,14 @@ export class ApRendererService {
|
||||||
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate {
|
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
||||||
actor: `${this.config.url}/users/${user.id}`,
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
object: {
|
object: {
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo: `${this.config.url}/users/${user.id}`,
|
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
inReplyTo: note.uri,
|
inReplyTo: note.uri,
|
||||||
name: poll.choices[vote.choice],
|
name: poll.choices[vote.choice],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -151,7 +151,7 @@ export class Resolver {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
|
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
|
||||||
)
|
)
|
||||||
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url)));
|
.then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url)));
|
||||||
default:
|
default:
|
||||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
import { User } from '@/models/entities/User.js';
|
import { User } from '@/models/entities/User.js';
|
||||||
import { truncate } from '@/misc/truncate.js';
|
import { truncate } from '@/misc/truncate.js';
|
||||||
import type { CacheService } from '@/core/CacheService.js';
|
import type { CacheService } from '@/core/CacheService.js';
|
||||||
|
@ -42,6 +42,7 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, IObject } from '../type.js';
|
import type { IActor, IObject } from '../type.js';
|
||||||
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
|
|
||||||
const nameLength = 128;
|
const nameLength = 128;
|
||||||
|
@ -67,6 +68,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
private usersChart: UsersChart;
|
private usersChart: UsersChart;
|
||||||
private instanceChart: InstanceChart;
|
private instanceChart: InstanceChart;
|
||||||
private apLoggerService: ApLoggerService;
|
private apLoggerService: ApLoggerService;
|
||||||
|
private accountMoveService: AccountMoveService;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -132,6 +134,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.usersChart = this.moduleRef.get('UsersChart');
|
this.usersChart = this.moduleRef.get('UsersChart');
|
||||||
this.instanceChart = this.moduleRef.get('InstanceChart');
|
this.instanceChart = this.moduleRef.get('InstanceChart');
|
||||||
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
||||||
|
this.accountMoveService = this.moduleRef.get('AccountMoveService');
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,27 +212,27 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Personをフェッチします。
|
* uriからUser(Person)をフェッチします。
|
||||||
*
|
*
|
||||||
* Misskeyに対象のPersonが登録されていればそれを返します。
|
* Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
|
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
const cached = this.cacheService.uriPersonCache.get(uri);
|
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(this.config.url + '/')) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const u = await this.usersRepository.findOneBy({ id });
|
const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
|
||||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.usersRepository.findOneBy({ uri });
|
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.cacheService.uriPersonCache.set(uri, exist);
|
this.cacheService.uriPersonCache.set(uri, exist);
|
||||||
|
@ -244,7 +247,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
* Personを作成します。
|
* Personを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createPerson(uri: string, resolver?: Resolver): Promise<User> {
|
public async createPerson(uri: string, resolver?: Resolver): Promise<RemoteUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
if (uri.startsWith(this.config.url)) {
|
||||||
|
@ -289,6 +292,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
movedToUri: person.movedTo,
|
movedToUri: person.movedTo,
|
||||||
|
movedAt: person.movedTo ? new Date() : null,
|
||||||
alsoKnownAs: person.alsoKnownAs,
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
|
@ -411,23 +415,26 @@ export class ApPersonService implements OnModuleInit {
|
||||||
/**
|
/**
|
||||||
* Personの情報を更新します。
|
* Personの情報を更新します。
|
||||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||||
|
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。
|
||||||
|
*
|
||||||
* @param uri URI of Person
|
* @param uri URI of Person
|
||||||
* @param resolver Resolver
|
* @param resolver Resolver
|
||||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
||||||
|
* @param movePreventUris ここに指定されたURIがPersonのmovedToに指定されていたり10回より多く回っている場合これ以上アカウント移行を行わない(無限ループ防止)
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
|
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser;
|
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||||
|
|
||||||
if (exist == null) {
|
if (exist === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -485,7 +492,16 @@ export class ApPersonService implements OnModuleInit {
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
} as Partial<User>;
|
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
|
const moving =
|
||||||
|
// 移行先がない→ある
|
||||||
|
(!exist.movedToUri && updates.movedToUri) ||
|
||||||
|
// 移行先がある→別のもの
|
||||||
|
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
|
||||||
|
// 移行先がある→ない、ない→ないは無視
|
||||||
|
|
||||||
|
if (moving) updates.movedAt = new Date();
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
updates.avatarId = avatar.id;
|
updates.avatarId = avatar.id;
|
||||||
|
@ -530,6 +546,31 @@ export class ApPersonService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||||
|
|
||||||
|
const updated = { ...exist, ...updates };
|
||||||
|
|
||||||
|
this.cacheService.uriPersonCache.set(uri, updated);
|
||||||
|
|
||||||
|
// 移行処理を行う
|
||||||
|
if (updated.movedAt && (
|
||||||
|
// 初めて移行する場合はmovedAtがnullなので移行処理を許可
|
||||||
|
exist.movedAt == null ||
|
||||||
|
// 以前のmovingから14日以上経過した場合のみ移行処理を許可
|
||||||
|
// (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく)
|
||||||
|
exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime()
|
||||||
|
)) {
|
||||||
|
this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`);
|
||||||
|
return this.processRemoteMove(updated, movePreventUris)
|
||||||
|
.then(result => {
|
||||||
|
this.logger.info(`Processing Move Finished [${result}] @${updated.username}@${updated.host} (${uri})`);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.logger.info(`Processing Move Failed @${updated.username}@${updated.host} (${uri})`, { stack: e });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'skip';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -539,7 +580,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
|
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
|
@ -614,4 +655,53 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* リモート由来のアカウント移行処理を行います
|
||||||
|
* @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提)
|
||||||
|
* @param movePreventUris ここに列挙されたURIにsrc.movedToUriが含まれる場合、移行処理はしない(無限ループ防止)
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
private async processRemoteMove(src: RemoteUser, movePreventUris: string[] = []): Promise<string> {
|
||||||
|
if (!src.movedToUri) return 'skip: no movedToUri';
|
||||||
|
if (src.uri === src.movedToUri) return 'skip: movedTo itself (src)'; // ???
|
||||||
|
if (movePreventUris.length > 10) return 'skip: too many moves';
|
||||||
|
|
||||||
|
// まずサーバー内で検索して様子見
|
||||||
|
let dst = await this.fetchPerson(src.movedToUri);
|
||||||
|
|
||||||
|
if (dst && this.userEntityService.isLocalUser(dst)) {
|
||||||
|
// targetがローカルユーザーだった場合データベースから引っ張ってくる
|
||||||
|
dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as LocalUser;
|
||||||
|
} else if (dst) {
|
||||||
|
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
||||||
|
|
||||||
|
// targetを見つけたことがあるならtargetをupdatePersonする
|
||||||
|
await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
|
||||||
|
dst = await this.fetchPerson(src.movedToUri) ?? dst;
|
||||||
|
} else {
|
||||||
|
if (src.movedToUri.startsWith(`${this.config.url}/`)) {
|
||||||
|
// ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
|
||||||
|
return 'failed: movedTo is local but not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
// targetが知らない人だったらresolvePerson
|
||||||
|
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
|
||||||
|
dst = await this.resolvePerson(src.movedToUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
||||||
|
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
||||||
|
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
||||||
|
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
|
||||||
|
return 'skip: dst.alsoKnownAs is empty';
|
||||||
|
}
|
||||||
|
if (!dst.alsoKnownAs?.includes(src.uri)) {
|
||||||
|
return 'skip: alsoKnownAs does not include from.uri';
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.accountMoveService.postMoveProcess(src, dst);
|
||||||
|
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
|
||||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js';
|
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -35,13 +34,13 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
|
||||||
const ajv = new Ajv();
|
const ajv = new Ajv();
|
||||||
|
|
||||||
function isLocalUser(user: User): user is LocalUser;
|
function isLocalUser(user: User): user is LocalUser;
|
||||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
function isLocalUser<T extends { host: User['host'] }>(user: T): user is (T & { host: null; });
|
||||||
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||||
return user.host == null;
|
return user.host == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemoteUser(user: User): user is RemoteUser;
|
function isRemoteUser(user: User): user is RemoteUser;
|
||||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is (T & { host: string; });
|
||||||
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||||
return !isLocalUser(user);
|
return !isLocalUser(user);
|
||||||
}
|
}
|
||||||
|
@ -280,6 +279,17 @@ export class UserEntityService implements OnModuleInit {
|
||||||
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
|
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getUserUri(user: LocalUser | PartialLocalUser | RemoteUser | PartialRemoteUser): string {
|
||||||
|
return this.isRemoteUser(user)
|
||||||
|
? user.uri : this.genLocalUserUri(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public genLocalUserUri(userId: string): string {
|
||||||
|
return `${this.config.url}/users/${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
|
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
|
||||||
src: User['id'] | User,
|
src: User['id'] | User,
|
||||||
me?: { id: User['id'] } | null | undefined,
|
me?: { id: User['id'] } | null | undefined,
|
||||||
|
@ -369,8 +379,11 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null,
|
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
||||||
alsoKnownAs: user.alsoKnownAs,
|
alsoKnownAs: user.alsoKnownAs
|
||||||
|
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
||||||
|
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[])
|
||||||
|
: null,
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||||
|
|
|
@ -75,6 +75,12 @@ export class User {
|
||||||
})
|
})
|
||||||
public movedToUri: string | null;
|
public movedToUri: string | null;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
comment: 'When the user moved to another account',
|
||||||
|
})
|
||||||
|
public movedAt: Date | null;
|
||||||
|
|
||||||
@Column('simple-array', {
|
@Column('simple-array', {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
comment: 'URIs the user is known as too',
|
comment: 'URIs the user is known as too',
|
||||||
|
@ -253,11 +259,23 @@ export type LocalUser = User & {
|
||||||
uri: null;
|
uri: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PartialLocalUser = Partial<User> & {
|
||||||
|
id: User['id'];
|
||||||
|
host: null;
|
||||||
|
uri: null;
|
||||||
|
}
|
||||||
|
|
||||||
export type RemoteUser = User & {
|
export type RemoteUser = User & {
|
||||||
host: string;
|
host: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PartialRemoteUser = Partial<User> & {
|
||||||
|
id: User['id'];
|
||||||
|
host: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
|
|
|
@ -84,7 +84,7 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'uri',
|
format: 'id',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
optional: false,
|
optional: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class RelationshipQueueProcessorsService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public start(q: Bull.Queue): void {
|
public start(q: Bull.Queue): void {
|
||||||
const maxJobs = (this.config.deliverJobConcurrency ?? 128) / 4; // conservative?
|
const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
|
||||||
q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
|
q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
|
||||||
q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
|
q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
|
||||||
q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
|
q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
|
||||||
|
|
|
@ -84,9 +84,9 @@ export class InboxProcessorService {
|
||||||
|
|
||||||
// HTTP-Signature keyIdを元にDBから取得
|
// HTTP-Signature keyIdを元にDBから取得
|
||||||
let authUser: {
|
let authUser: {
|
||||||
user: RemoteUser;
|
user: RemoteUser;
|
||||||
key: UserPublickey | null;
|
key: UserPublickey | null;
|
||||||
} | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
|
} | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
|
||||||
|
|
||||||
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
||||||
if (authUser == null) {
|
if (authUser == null) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import { RelationshipJobData } from '../types.js';
|
import { RelationshipJobData } from '../types.js';
|
||||||
import type { UsersRepository } from '@/models/index.js';
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RelationshipProcessorService {
|
export class RelationshipProcessorService {
|
||||||
|
@ -39,7 +40,7 @@ export class RelationshipProcessorService {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||||
]);
|
]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
|
||||||
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
|
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import type { Following } from '@/models/entities/Following.js';
|
import type { Following } from '@/models/entities/Following.js';
|
||||||
import { countIf } from '@/misc/prelude/array.js';
|
import { countIf } from '@/misc/prelude/array.js';
|
||||||
|
@ -630,7 +630,7 @@ export class ActivityPubServerService {
|
||||||
id: request.params.followee,
|
id: request.params.followee,
|
||||||
host: Not(IsNull()),
|
host: Not(IsNull()),
|
||||||
}),
|
}),
|
||||||
]);
|
]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
|
||||||
|
|
||||||
if (follower == null || followee == null) {
|
if (follower == null || followee == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
|
@ -665,7 +665,7 @@ export class ActivityPubServerService {
|
||||||
id: followRequest.followeeId,
|
id: followRequest.followeeId,
|
||||||
host: Not(IsNull()),
|
host: Not(IsNull()),
|
||||||
}),
|
}),
|
||||||
]);
|
]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
|
||||||
|
|
||||||
if (follower == null || followee == null) {
|
if (follower == null || followee == null) {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||||
|
@ -23,6 +24,7 @@ export class WellKnownServerService {
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
private nodeinfoServerService: NodeinfoServerService,
|
private nodeinfoServerService: NodeinfoServerService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +132,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
const self = {
|
const self = {
|
||||||
rel: 'self',
|
rel: 'self',
|
||||||
type: 'application/activity+json',
|
type: 'application/activity+json',
|
||||||
href: `${this.config.url}/users/${user.id}`,
|
href: this.userEntityService.genLocalUserUri(user.id),
|
||||||
};
|
};
|
||||||
const profilePage = {
|
const profilePage = {
|
||||||
rel: 'http://webfinger.net/rel/profile-page',
|
rel: 'http://webfinger.net/rel/profile-page',
|
||||||
|
|
|
@ -261,6 +261,17 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ep.meta.prohibitMoved) {
|
||||||
|
if (user?.movedToUri) {
|
||||||
|
throw new ApiError({
|
||||||
|
message: 'You have moved your account.',
|
||||||
|
code: 'YOUR_ACCOUNT_MOVED',
|
||||||
|
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
|
||||||
|
httpStatusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
|
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
|
||||||
const myRoles = await this.roleService.getUserRoles(user!.id);
|
const myRoles = await this.roleService.getUserRoles(user!.id);
|
||||||
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
|
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
|
||||||
|
|
|
@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||||
import * as ep___i_update from './endpoints/i/update.js';
|
import * as ep___i_update from './endpoints/i/update.js';
|
||||||
import * as ep___i_move from './endpoints/i/move.js';
|
import * as ep___i_move from './endpoints/i/move.js';
|
||||||
import * as ep___i_knownAs from './endpoints/i/known-as.js';
|
|
||||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||||
|
@ -560,7 +559,6 @@ const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.defau
|
||||||
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
|
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
|
||||||
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
|
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
|
||||||
const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
|
const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
|
||||||
const $i_knownAs: Provider = { provide: 'ep:i/known-as', useClass: ep___i_knownAs.default };
|
|
||||||
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
|
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
|
||||||
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
|
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
|
||||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||||
|
@ -901,7 +899,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_updateEmail,
|
$i_updateEmail,
|
||||||
$i_update,
|
$i_update,
|
||||||
$i_move,
|
$i_move,
|
||||||
$i_knownAs,
|
|
||||||
$i_webhooks_create,
|
$i_webhooks_create,
|
||||||
$i_webhooks_list,
|
$i_webhooks_list,
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
|
@ -1236,7 +1233,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_updateEmail,
|
$i_updateEmail,
|
||||||
$i_update,
|
$i_update,
|
||||||
$i_move,
|
$i_move,
|
||||||
$i_knownAs,
|
|
||||||
$i_webhooks_create,
|
$i_webhooks_create,
|
||||||
$i_webhooks_list,
|
$i_webhooks_list,
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
|
|
|
@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||||
import * as ep___i_update from './endpoints/i/update.js';
|
import * as ep___i_update from './endpoints/i/update.js';
|
||||||
import * as ep___i_move from './endpoints/i/move.js';
|
import * as ep___i_move from './endpoints/i/move.js';
|
||||||
import * as ep___i_knownAs from './endpoints/i/known-as.js';
|
|
||||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||||
|
@ -557,8 +556,7 @@ const eps = [
|
||||||
['i/unpin', ep___i_unpin],
|
['i/unpin', ep___i_unpin],
|
||||||
['i/update-email', ep___i_updateEmail],
|
['i/update-email', ep___i_updateEmail],
|
||||||
['i/update', ep___i_update],
|
['i/update', ep___i_update],
|
||||||
//['i/move', ep___i_move],
|
['i/move', ep___i_move],
|
||||||
//['i/known-as', ep___i_knownAs],
|
|
||||||
['i/webhooks/create', ep___i_webhooks_create],
|
['i/webhooks/create', ep___i_webhooks_create],
|
||||||
['i/webhooks/list', ep___i_webhooks_list],
|
['i/webhooks/list', ep___i_webhooks_list],
|
||||||
['i/webhooks/show', ep___i_webhooks_show],
|
['i/webhooks/show', ep___i_webhooks_show],
|
||||||
|
@ -704,6 +702,12 @@ export interface IEndpointMeta {
|
||||||
|
|
||||||
readonly requireRolePolicy?: keyof RolePolicies;
|
readonly requireRolePolicy?: keyof RolePolicies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
||||||
|
* 省略した場合は false として解釈されます。
|
||||||
|
*/
|
||||||
|
readonly prohibitMoved?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* エンドポイントのリミテーションに関するやつ
|
* エンドポイントのリミテーションに関するやつ
|
||||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:channels',
|
kind: 'write:channels',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:clip-favorite',
|
kind: 'write:clip-favorite',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:clip-favorite',
|
kind: 'write:clip-favorite',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -15,6 +15,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 120,
|
max: 120,
|
||||||
|
|
|
@ -19,6 +19,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:drive',
|
kind: 'write:drive',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:flash',
|
kind: 'write:flash',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:flash-likes',
|
kind: 'write:flash-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:flash-likes',
|
kind: 'write:flash-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:flash',
|
kind: 'write:flash',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -19,6 +19,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:following',
|
kind: 'write:following',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:gallery',
|
kind: 'write:gallery',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:gallery-likes',
|
kind: 'write:gallery-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:gallery-likes',
|
kind: 'write:gallery-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:gallery',
|
kind: 'write:gallery',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
secure: true,
|
secure: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
|
@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||||
if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
|
|
||||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||||
|
|
||||||
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
|
me,
|
||||||
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
||||||
this.queueService.createImportBlockingJob(me, file.id);
|
this.queueService.createImportBlockingJob(me, file.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
secure: true,
|
secure: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 1,
|
max: 1,
|
||||||
|
@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||||
if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
|
|
||||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||||
|
|
||||||
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
|
me,
|
||||||
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
||||||
this.queueService.createImportFollowingJob(me, file.id);
|
this.queueService.createImportFollowingJob(me, file.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
secure: true,
|
secure: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
|
@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||||
if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
|
|
||||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||||
|
|
||||||
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
|
me,
|
||||||
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
||||||
this.queueService.createImportMutingJob(me, file.id);
|
this.queueService.createImportMutingJob(me, file.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
secure: true,
|
secure: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 1,
|
max: 1,
|
||||||
|
@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
|
||||||
if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
|
|
||||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||||
|
|
||||||
|
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
|
||||||
|
me,
|
||||||
|
(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
|
||||||
|
|
||||||
this.queueService.createImportUserListsJob(me, file.id);
|
this.queueService.createImportUserListsJob(me, file.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import ms from 'ms';
|
|
||||||
|
|
||||||
import { User } from '@/models/entities/User.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
|
||||||
import { ApiError } from '@/server/api/error.js';
|
|
||||||
|
|
||||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
|
||||||
tags: ['users'],
|
|
||||||
|
|
||||||
secure: true,
|
|
||||||
requireCredential: true,
|
|
||||||
|
|
||||||
limit: {
|
|
||||||
duration: ms('1day'),
|
|
||||||
max: 30,
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
noSuchUser: {
|
|
||||||
message: 'No such user.',
|
|
||||||
code: 'NO_SUCH_USER',
|
|
||||||
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
|
||||||
},
|
|
||||||
notRemote: {
|
|
||||||
message: 'User is not remote. You can only migrate from other instances.',
|
|
||||||
code: 'NOT_REMOTE',
|
|
||||||
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
|
||||||
},
|
|
||||||
uriNull: {
|
|
||||||
message: 'User ActivityPup URI is null.',
|
|
||||||
code: 'URI_NULL',
|
|
||||||
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const paramDef = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
alsoKnownAs: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['alsoKnownAs'],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|
||||||
constructor(
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private remoteUserResolveService: RemoteUserResolveService,
|
|
||||||
private apiLoggerService: ApiLoggerService,
|
|
||||||
private accountMoveService: AccountMoveService,
|
|
||||||
) {
|
|
||||||
super(meta, paramDef, async (ps, me) => {
|
|
||||||
// Check parameter
|
|
||||||
if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
|
|
||||||
|
|
||||||
let unfiltered = ps.alsoKnownAs;
|
|
||||||
const updates = {} as Partial<User>;
|
|
||||||
|
|
||||||
if (!unfiltered) {
|
|
||||||
updates.alsoKnownAs = null;
|
|
||||||
} else {
|
|
||||||
// Parse user's input into the old account
|
|
||||||
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
|
|
||||||
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
|
||||||
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
|
||||||
|
|
||||||
const userAddress = unfiltered.split('@');
|
|
||||||
// Retrieve the old account
|
|
||||||
const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
|
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
const toUrl: string | null = knownAs.uri;
|
|
||||||
if (!toUrl) throw new ApiError(meta.errors.uriNull);
|
|
||||||
// Only allow moving from a remote account
|
|
||||||
if (this.userEntityService.isLocalUser(knownAs)) throw new ApiError(meta.errors.notRemote);
|
|
||||||
|
|
||||||
updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.accountMoveService.createAlias(me, updates);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,40 +7,35 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
|
import { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||||
|
|
||||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
|
||||||
|
import * as Acct from '@/misc/acct.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users'],
|
tags: ['users'],
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1day'),
|
duration: ms('1day'),
|
||||||
max: 5,
|
max: 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
noSuchMoveTarget: {
|
destinationAccountForbids: {
|
||||||
message: 'No such move target.',
|
|
||||||
code: 'NO_SUCH_MOVE_TARGET',
|
|
||||||
id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
|
|
||||||
},
|
|
||||||
remoteAccountForbids: {
|
|
||||||
message:
|
message:
|
||||||
'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
|
'Destination account doesn\'t have proper \'Known As\' alias, or has already moved.',
|
||||||
code: 'REMOTE_ACCOUNT_FORBIDS',
|
code: 'DESTINATION_ACCOUNT_FORBIDS',
|
||||||
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
|
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
|
||||||
},
|
},
|
||||||
notRemote: {
|
|
||||||
message: 'User is not remote. You can only migrate to other instances.',
|
|
||||||
code: 'NOT_REMOTE',
|
|
||||||
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
|
||||||
},
|
|
||||||
rootForbidden: {
|
rootForbidden: {
|
||||||
message: 'The root can\'t migrate.',
|
message: 'The root can\'t migrate.',
|
||||||
code: 'NOT_ROOT_FORBIDDEN',
|
code: 'NOT_ROOT_FORBIDDEN',
|
||||||
|
@ -84,57 +79,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private remoteUserResolveService: RemoteUserResolveService,
|
private remoteUserResolveService: RemoteUserResolveService,
|
||||||
private apiLoggerService: ApiLoggerService,
|
private apiLoggerService: ApiLoggerService,
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// check parameter
|
// check parameter
|
||||||
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
|
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
|
||||||
// abort if user is the root
|
// abort if user is the root
|
||||||
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
|
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
|
||||||
// abort if user has already moved
|
// abort if user has already moved
|
||||||
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
|
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
|
||||||
|
|
||||||
let unfiltered = ps.moveToAccount;
|
|
||||||
if (!unfiltered) throw new ApiError(meta.errors.noSuchMoveTarget);
|
|
||||||
|
|
||||||
// parse user's input into the destination account
|
// parse user's input into the destination account
|
||||||
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
|
const { username, host } = Acct.parse(ps.moveToAccount);
|
||||||
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
|
||||||
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
|
||||||
|
|
||||||
const userAddress = unfiltered.split('@');
|
|
||||||
// retrieve the destination account
|
// retrieve the destination account
|
||||||
let moveTo = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
|
let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
||||||
throw new ApiError(meta.errors.noSuchMoveTarget);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
});
|
});
|
||||||
const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id);
|
const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser;
|
||||||
if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull);
|
const newUri = this.userEntityService.getUserUri(destination);
|
||||||
|
|
||||||
// update local db
|
// update local db
|
||||||
await this.apPersonService.updatePerson(remoteMoveTo.uri);
|
await this.apPersonService.updatePerson(newUri);
|
||||||
// retrieve updated user
|
// retrieve updated user
|
||||||
moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri);
|
moveTo = await this.apPersonService.resolvePerson(newUri);
|
||||||
// only allow moving to a remote account
|
|
||||||
if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote);
|
|
||||||
|
|
||||||
let allowed = false;
|
|
||||||
|
|
||||||
const fromUrl = `${this.config.url}/users/${me.id}`;
|
|
||||||
// make sure that the user has indicated the old account as an alias
|
// make sure that the user has indicated the old account as an alias
|
||||||
moveTo.alsoKnownAs?.forEach((elem) => {
|
const fromUrl = this.userEntityService.genLocalUserUri(me.id);
|
||||||
if (fromUrl.includes(elem)) allowed = true;
|
let allowed = false;
|
||||||
});
|
if (moveTo.alsoKnownAs) {
|
||||||
|
for (const knownAs of moveTo.alsoKnownAs) {
|
||||||
|
if (knownAs.includes(fromUrl)) {
|
||||||
|
allowed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// abort if unintended
|
// abort if unintended
|
||||||
if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids);
|
if (!allowed || moveTo.movedToUri) throw new ApiError(meta.errors.destinationAccountForbids);
|
||||||
|
|
||||||
return await this.accountMoveService.moveToRemote(me, moveTo);
|
return await this.accountMoveService.moveFromLocal(me, moveTo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
||||||
tags: ['account', 'notes'],
|
tags: ['account', 'notes'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js';
|
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js';
|
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js';
|
||||||
|
@ -19,7 +20,10 @@ import { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
|
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -71,6 +75,24 @@ export const meta = {
|
||||||
code: 'TOO_MANY_MUTED_WORDS',
|
code: 'TOO_MANY_MUTED_WORDS',
|
||||||
id: '010665b1-a211-42d2-bc64-8f6609d79785',
|
id: '010665b1-a211-42d2-bc64-8f6609d79785',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||||
|
},
|
||||||
|
|
||||||
|
uriNull: {
|
||||||
|
message: 'User ActivityPup URI is null.',
|
||||||
|
code: 'URI_NULL',
|
||||||
|
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
||||||
|
},
|
||||||
|
|
||||||
|
forbiddenToSetYourself: {
|
||||||
|
message: 'You can\'t set yourself as your own alias.',
|
||||||
|
code: 'FORBIDDEN_TO_SET_YOURSELF',
|
||||||
|
id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
@ -129,6 +151,12 @@ export const paramDef = {
|
||||||
emailNotificationTypes: { type: 'array', items: {
|
emailNotificationTypes: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
alsoKnownAs: {
|
||||||
|
type: 'array',
|
||||||
|
maxItems: 10,
|
||||||
|
uniqueItems: true,
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -153,6 +181,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private accountUpdateService: AccountUpdateService,
|
private accountUpdateService: AccountUpdateService,
|
||||||
|
private accountMoveService: AccountMoveService,
|
||||||
|
private remoteUserResolveService: RemoteUserResolveService,
|
||||||
|
private apiLoggerService: ApiLoggerService,
|
||||||
private hashtagService: HashtagService,
|
private hashtagService: HashtagService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
@ -260,6 +291,38 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.alsoKnownAs) {
|
||||||
|
if (_user.movedToUri) {
|
||||||
|
throw new ApiError({
|
||||||
|
message: 'You have moved your account.',
|
||||||
|
code: 'YOUR_ACCOUNT_MOVED',
|
||||||
|
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
|
||||||
|
httpStatusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse user's input into the old account
|
||||||
|
const newAlsoKnownAs = new Set<string>();
|
||||||
|
for (const line of ps.alsoKnownAs) {
|
||||||
|
if (!line) throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
const { username, host } = Acct.parse(line);
|
||||||
|
|
||||||
|
// Retrieve the old account
|
||||||
|
const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
||||||
|
this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
});
|
||||||
|
if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
|
||||||
|
|
||||||
|
const toUrl = this.userEntityService.getUserUri(knownAs);
|
||||||
|
if (!toUrl) throw new ApiError(meta.errors.uriNull);
|
||||||
|
|
||||||
|
newAlsoKnownAs.add(toUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
|
||||||
|
}
|
||||||
|
|
||||||
//#region emojis/tags
|
//#region emojis/tags
|
||||||
|
|
||||||
let emojis = [] as string[];
|
let emojis = [] as string[];
|
||||||
|
@ -287,6 +350,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
|
if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
|
||||||
|
if (Object.keys(updates).includes('alsoKnownAs')) {
|
||||||
|
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
|
||||||
|
}
|
||||||
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
|
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
|
||||||
|
|
||||||
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
|
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:mutes',
|
kind: 'write:mutes',
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 300,
|
max: 300,
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const meta = {
|
||||||
tags: ['notes', 'favorites'],
|
tags: ['notes', 'favorites'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:favorites',
|
kind: 'write:favorites',
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:votes',
|
kind: 'write:votes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:reactions',
|
kind: 'write:reactions',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:pages',
|
kind: 'write:pages',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:page-likes',
|
kind: 'write:page-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:page-likes',
|
kind: 'write:page-likes',
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:pages',
|
kind: 'write:pages',
|
||||||
|
|
||||||
limit: {
|
limit: {
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:mutes',
|
kind: 'write:mutes',
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
description: 'Create a new list of users.',
|
description: 'Create a new list of users.',
|
||||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
description: 'Remove a user from a list.',
|
description: 'Remove a user from a list.',
|
||||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
|
prohibitMoved: true,
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
description: 'Add a user to an existing list.',
|
description: 'Add a user to an existing list.',
|
||||||
|
|
|
@ -4,8 +4,9 @@ import * as assert from 'assert';
|
||||||
// node-fetch only supports it's own Blob yet
|
// node-fetch only supports it's own Blob yet
|
||||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||||
import { Blob } from 'node-fetch';
|
import { Blob } from 'node-fetch';
|
||||||
import { startServer, signup, post, api, uploadFile, simpleGet } from '../utils.js';
|
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
import { User } from '@/models/index.js';
|
||||||
|
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
@ -289,6 +290,16 @@ describe('Endpoints', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
|
||||||
|
const connection = await initTestDb(true);
|
||||||
|
const Users = connection.getRepository(User);
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.followersCount, 0);
|
||||||
|
assert.strictEqual(newBob.followingCount, 1);
|
||||||
|
const newAlice = await Users.findOneByOrFail({ id: alice.id });
|
||||||
|
assert.strictEqual(newAlice.followersCount, 1);
|
||||||
|
assert.strictEqual(newAlice.followingCount, 0);
|
||||||
|
connection.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('既にフォローしている場合は怒る', async () => {
|
test('既にフォローしている場合は怒る', async () => {
|
||||||
|
@ -341,6 +352,16 @@ describe('Endpoints', () => {
|
||||||
}, bob);
|
}, bob);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
|
||||||
|
const connection = await initTestDb(true);
|
||||||
|
const Users = connection.getRepository(User);
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.followersCount, 0);
|
||||||
|
assert.strictEqual(newBob.followingCount, 0);
|
||||||
|
const newAlice = await Users.findOneByOrFail({ id: alice.id });
|
||||||
|
assert.strictEqual(newAlice.followersCount, 0);
|
||||||
|
assert.strictEqual(newAlice.followingCount, 0);
|
||||||
|
connection.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('フォローしていない場合は怒る', async () => {
|
test('フォローしていない場合は怒る', async () => {
|
||||||
|
|
455
packages/backend/test/e2e/move.ts
Normal file
455
packages/backend/test/e2e/move.ts
Normal file
|
@ -0,0 +1,455 @@
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import rndstr from 'rndstr';
|
||||||
|
import { loadConfig } from '@/config.js';
|
||||||
|
import { User, UsersRepository } from '@/models/index.js';
|
||||||
|
import { jobQueue } from '@/boot/common.js';
|
||||||
|
import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js';
|
||||||
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
describe('Account Move', () => {
|
||||||
|
let app: INestApplicationContext;
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
let root: any;
|
||||||
|
let alice: any;
|
||||||
|
let bob: any;
|
||||||
|
let carol: any;
|
||||||
|
let dave: any;
|
||||||
|
let eve: any;
|
||||||
|
let frank: any;
|
||||||
|
|
||||||
|
let Users: UsersRepository;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await startServer();
|
||||||
|
await jobQueue();
|
||||||
|
const config = loadConfig();
|
||||||
|
url = new URL(config.url);
|
||||||
|
const connection = await initTestDb(false);
|
||||||
|
root = await signup({ username: 'root' });
|
||||||
|
alice = await signup({ username: 'alice' });
|
||||||
|
bob = await signup({ username: 'bob' });
|
||||||
|
carol = await signup({ username: 'carol' });
|
||||||
|
dave = await signup({ username: 'dave' });
|
||||||
|
eve = await signup({ username: 'eve' });
|
||||||
|
frank = await signup({ username: 'frank' });
|
||||||
|
Users = connection.getRepository(User);
|
||||||
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Create Alias', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await Users.update(bob.id, { alsoKnownAs: null });
|
||||||
|
}, 1000 * 10);
|
||||||
|
|
||||||
|
test('Able to create an alias', async () => {
|
||||||
|
const res = await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs?.length, 1);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
|
||||||
|
assert.strictEqual(res.body.alsoKnownAs?.length, 1);
|
||||||
|
assert.strictEqual(res.body.alsoKnownAs[0], alice.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Able to create a local alias without hostname', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: ['@alice'],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs?.length, 1);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Able to create a local alias without @', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: ['alice'],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs?.length, 1);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Able to set remote user (but may fail)', async () => {
|
||||||
|
const res = await api('/i/update', {
|
||||||
|
alsoKnownAs: ['@syuilo@example.com'],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
||||||
|
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
|
||||||
|
const res = await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
|
||||||
|
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to add itself', async () => {
|
||||||
|
const res = await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@bob@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF');
|
||||||
|
assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
|
||||||
|
const res1 = await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@nonexist@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res1.status, 400);
|
||||||
|
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
|
||||||
|
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
|
||||||
|
const res2 = await api('/i/update', {
|
||||||
|
alsoKnownAs: ['@alice', 'nonexist'],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res2.status, 400);
|
||||||
|
assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER');
|
||||||
|
assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Able to add two existing local account to alsoKnownAs', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs?.length, 2);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${carol.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Able to properly overwrite alsoKnownAs', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
const newBob = await Users.findOneByOrFail({ id: bob.id });
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs?.length, 2);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${carol.id}`);
|
||||||
|
assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${dave.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Local to Local', () => {
|
||||||
|
let antennaId = '';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`],
|
||||||
|
}, root);
|
||||||
|
const listRoot = await api('/users/lists/create', {
|
||||||
|
name: rndstr('0-9a-z', 8),
|
||||||
|
}, root);
|
||||||
|
await api('/users/lists/push', {
|
||||||
|
listId: listRoot.body.id,
|
||||||
|
userId: alice.id,
|
||||||
|
}, root);
|
||||||
|
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: root.id,
|
||||||
|
}, alice);
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: eve.id,
|
||||||
|
}, alice);
|
||||||
|
const antenna = await api('/antennas/create', {
|
||||||
|
name: rndstr('0-9a-z', 8),
|
||||||
|
src: 'home',
|
||||||
|
keywords: [rndstr('0-9a-z', 8)],
|
||||||
|
excludeKeywords: [],
|
||||||
|
users: [],
|
||||||
|
caseSensitive: false,
|
||||||
|
withReplies: false,
|
||||||
|
withFile: false,
|
||||||
|
notify: false,
|
||||||
|
}, alice);
|
||||||
|
antennaId = antenna.body.id;
|
||||||
|
|
||||||
|
await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@alice@${url.hostname}`],
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, carol);
|
||||||
|
|
||||||
|
await api('/mute/create', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, dave);
|
||||||
|
await api('/blocking/create', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, dave);
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: eve.id,
|
||||||
|
}, dave);
|
||||||
|
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: dave.id,
|
||||||
|
}, eve);
|
||||||
|
const listEve = await api('/users/lists/create', {
|
||||||
|
name: rndstr('0-9a-z', 8),
|
||||||
|
}, eve);
|
||||||
|
await api('/users/lists/push', {
|
||||||
|
listId: listEve.body.id,
|
||||||
|
userId: bob.id,
|
||||||
|
}, eve);
|
||||||
|
|
||||||
|
await api('/i/update', {
|
||||||
|
isLocked: true,
|
||||||
|
}, frank);
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: frank.id,
|
||||||
|
}, alice);
|
||||||
|
await api('/following/requests/accept', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, frank);
|
||||||
|
}, 1000 * 10);
|
||||||
|
|
||||||
|
test('Prohibit the root account from moving', async () => {
|
||||||
|
const res = await api('/i/move', {
|
||||||
|
moveToAccount: `@bob@${url.hostname}`,
|
||||||
|
}, root);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN');
|
||||||
|
assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to move to a nonexisting local account', async () => {
|
||||||
|
const res = await api('/i/move', {
|
||||||
|
moveToAccount: `@nonexist@${url.hostname}`,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'NO_SUCH_USER');
|
||||||
|
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to move if alsoKnownAs is invalid', async () => {
|
||||||
|
const res = await api('/i/move', {
|
||||||
|
moveToAccount: `@carol@${url.hostname}`,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
|
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Relationships have been properly migrated', async () => {
|
||||||
|
const move = await api('/i/move', {
|
||||||
|
moveToAccount: `@bob@${url.hostname}`,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(move.status, 200);
|
||||||
|
|
||||||
|
await sleep(1000 * 3); // wait for jobs to finish
|
||||||
|
|
||||||
|
// Unfollow delayed?
|
||||||
|
const aliceFollowings = await api('/users/following', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(aliceFollowings.status, 200);
|
||||||
|
assert.strictEqual(aliceFollowings.body.length, 3);
|
||||||
|
|
||||||
|
const carolFollowings = await api('/users/following', {
|
||||||
|
userId: carol.id,
|
||||||
|
}, carol);
|
||||||
|
assert.strictEqual(carolFollowings.status, 200);
|
||||||
|
assert.strictEqual(carolFollowings.body.length, 2);
|
||||||
|
assert.strictEqual(carolFollowings.body[0].followeeId, bob.id);
|
||||||
|
assert.strictEqual(carolFollowings.body[1].followeeId, alice.id);
|
||||||
|
|
||||||
|
const blockings = await api('/blocking/list', {}, dave);
|
||||||
|
assert.strictEqual(blockings.status, 200);
|
||||||
|
assert.strictEqual(blockings.body.length, 2);
|
||||||
|
assert.strictEqual(blockings.body[0].blockeeId, bob.id);
|
||||||
|
assert.strictEqual(blockings.body[1].blockeeId, alice.id);
|
||||||
|
|
||||||
|
const mutings = await api('/mute/list', {}, dave);
|
||||||
|
assert.strictEqual(mutings.status, 200);
|
||||||
|
assert.strictEqual(mutings.body.length, 2);
|
||||||
|
assert.strictEqual(mutings.body[0].muteeId, bob.id);
|
||||||
|
assert.strictEqual(mutings.body[1].muteeId, alice.id);
|
||||||
|
|
||||||
|
const rootLists = await api('/users/lists/list', {}, root);
|
||||||
|
assert.strictEqual(rootLists.status, 200);
|
||||||
|
assert.strictEqual(rootLists.body[0].userIds.length, 2);
|
||||||
|
assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||||
|
assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id));
|
||||||
|
|
||||||
|
const eveLists = await api('/users/lists/list', {}, eve);
|
||||||
|
assert.strictEqual(eveLists.status, 200);
|
||||||
|
assert.strictEqual(eveLists.body[0].userIds.length, 1);
|
||||||
|
assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('A locked account automatically accept the follow request if it had already accepted the old account.', async () => {
|
||||||
|
await successfulApiCall({
|
||||||
|
endpoint: '/following/create',
|
||||||
|
parameters: {
|
||||||
|
userId: frank.id,
|
||||||
|
},
|
||||||
|
user: bob,
|
||||||
|
});
|
||||||
|
const followers = await api('/users/followers', {
|
||||||
|
userId: frank.id,
|
||||||
|
}, frank);
|
||||||
|
|
||||||
|
assert.strictEqual(followers.status, 200);
|
||||||
|
assert.strictEqual(followers.body.length, 2);
|
||||||
|
assert.strictEqual(followers.body[0].followerId, bob.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unfollowed after 10 sec (24 hours in production).', async () => {
|
||||||
|
await sleep(1000 * 8);
|
||||||
|
|
||||||
|
const following = await api('/users/following', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(following.status, 200);
|
||||||
|
assert.strictEqual(following.body.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unable to move if the destination account has already moved.', async () => {
|
||||||
|
const res = await api('/i/move', {
|
||||||
|
moveToAccount: `@alice@${url.hostname}`,
|
||||||
|
}, bob);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 400);
|
||||||
|
assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS');
|
||||||
|
assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Follow and follower counts are properly adjusted', async () => {
|
||||||
|
await api('/following/create', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, eve);
|
||||||
|
const newAlice = await Users.findOneByOrFail({ id: alice.id });
|
||||||
|
const newCarol = await Users.findOneByOrFail({ id: carol.id });
|
||||||
|
let newEve = await Users.findOneByOrFail({ id: eve.id });
|
||||||
|
assert.strictEqual(newAlice.movedToUri, `${url.origin}/users/${bob.id}`);
|
||||||
|
assert.strictEqual(newAlice.followingCount, 0);
|
||||||
|
assert.strictEqual(newAlice.followersCount, 0);
|
||||||
|
assert.strictEqual(newCarol.followingCount, 1);
|
||||||
|
assert.strictEqual(newEve.followingCount, 1);
|
||||||
|
assert.strictEqual(newEve.followersCount, 1);
|
||||||
|
|
||||||
|
await api('/following/delete', {
|
||||||
|
userId: alice.id,
|
||||||
|
}, eve);
|
||||||
|
newEve = await Users.findOneByOrFail({ id: eve.id });
|
||||||
|
assert.strictEqual(newEve.followingCount, 1);
|
||||||
|
assert.strictEqual(newEve.followersCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
'/antennas/create',
|
||||||
|
'/channels/create',
|
||||||
|
'/channels/favorite',
|
||||||
|
'/channels/follow',
|
||||||
|
'/channels/unfavorite',
|
||||||
|
'/channels/unfollow',
|
||||||
|
'/clips/add-note',
|
||||||
|
'/clips/create',
|
||||||
|
'/clips/favorite',
|
||||||
|
'/clips/remove-note',
|
||||||
|
'/clips/unfavorite',
|
||||||
|
'/clips/update',
|
||||||
|
'/drive/files/upload-from-url',
|
||||||
|
'/flash/create',
|
||||||
|
'/flash/like',
|
||||||
|
'/flash/unlike',
|
||||||
|
'/flash/update',
|
||||||
|
'/following/create',
|
||||||
|
'/gallery/posts/create',
|
||||||
|
'/gallery/posts/like',
|
||||||
|
'/gallery/posts/unlike',
|
||||||
|
'/gallery/posts/update',
|
||||||
|
'/i/claim-achievement',
|
||||||
|
'/i/move',
|
||||||
|
'/i/import-blocking',
|
||||||
|
'/i/import-following',
|
||||||
|
'/i/import-muting',
|
||||||
|
'/i/import-user-lists',
|
||||||
|
'/i/pin',
|
||||||
|
'/mute/create',
|
||||||
|
'/notes/create',
|
||||||
|
'/notes/favorites/create',
|
||||||
|
'/notes/polls/vote',
|
||||||
|
'/notes/reactions/create',
|
||||||
|
'/pages/create',
|
||||||
|
'/pages/like',
|
||||||
|
'/pages/unlike',
|
||||||
|
'/pages/update',
|
||||||
|
'/renote-mute/create',
|
||||||
|
'/users/lists/create',
|
||||||
|
'/users/lists/pull',
|
||||||
|
'/users/lists/push',
|
||||||
|
])('Prohibit access after moving: %s', async (endpoint) => {
|
||||||
|
const res = await api(endpoint, {}, alice);
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Prohibit access after moving: /antennas/update', async () => {
|
||||||
|
const res = await api('/antennas/update', {
|
||||||
|
antennaId,
|
||||||
|
name: rndstr('0-9a-z', 8),
|
||||||
|
src: 'users',
|
||||||
|
keywords: [rndstr('0-9a-z', 8)],
|
||||||
|
excludeKeywords: [],
|
||||||
|
users: [eve.id],
|
||||||
|
caseSensitive: false,
|
||||||
|
withReplies: false,
|
||||||
|
withFile: false,
|
||||||
|
notify: false,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Prohibit access after moving: /drive/files/create', async () => {
|
||||||
|
const res = await uploadFile(alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Prohibit updating alsoKnownAs after moving', async () => {
|
||||||
|
const res = await api('/i/update', {
|
||||||
|
alsoKnownAs: [`@eve@${url.hostname}`],
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 403);
|
||||||
|
assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED');
|
||||||
|
assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -83,7 +83,7 @@ describe('ユーザー', () => {
|
||||||
...userLite(user),
|
...userLite(user),
|
||||||
url: user.url,
|
url: user.url,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
movedToUri: user.movedToUri,
|
movedTo: user.movedTo,
|
||||||
alsoKnownAs: user.alsoKnownAs,
|
alsoKnownAs: user.alsoKnownAs,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
|
@ -348,7 +348,7 @@ describe('ユーザー', () => {
|
||||||
// UserDetailedNotMeOnly
|
// UserDetailedNotMeOnly
|
||||||
assert.strictEqual(response.url, null);
|
assert.strictEqual(response.url, null);
|
||||||
assert.strictEqual(response.uri, null);
|
assert.strictEqual(response.uri, null);
|
||||||
assert.strictEqual(response.movedToUri, null);
|
assert.strictEqual(response.movedTo, null);
|
||||||
assert.strictEqual(response.alsoKnownAs, null);
|
assert.strictEqual(response.alsoKnownAs, null);
|
||||||
assert.strictEqual(response.createdAt, new Date(response.createdAt).toISOString());
|
assert.strictEqual(response.createdAt, new Date(response.createdAt).toISOString());
|
||||||
assert.strictEqual(response.updatedAt, null);
|
assert.strictEqual(response.updatedAt, null);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { RelaysRepository } from '@/models/index.js';
|
import type { RelaysRepository } from '@/models/index.js';
|
||||||
|
@ -21,6 +22,7 @@ describe('RelayService', () => {
|
||||||
let relayService: RelayService;
|
let relayService: RelayService;
|
||||||
let queueService: jest.Mocked<QueueService>;
|
let queueService: jest.Mocked<QueueService>;
|
||||||
let relaysRepository: RelaysRepository;
|
let relaysRepository: RelaysRepository;
|
||||||
|
let userEntityService: UserEntityService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await Test.createTestingModule({
|
app = await Test.createTestingModule({
|
||||||
|
@ -32,6 +34,7 @@ describe('RelayService', () => {
|
||||||
CreateSystemUserService,
|
CreateSystemUserService,
|
||||||
ApRendererService,
|
ApRendererService,
|
||||||
RelayService,
|
RelayService,
|
||||||
|
UserEntityService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.useMocker((token) => {
|
.useMocker((token) => {
|
||||||
|
@ -51,6 +54,7 @@ describe('RelayService', () => {
|
||||||
relayService = app.get<RelayService>(RelayService);
|
relayService = app.get<RelayService>(RelayService);
|
||||||
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
|
||||||
relaysRepository = app.get<RelaysRepository>(DI.relaysRepository);
|
relaysRepository = app.get<RelaysRepository>(DI.relaysRepository);
|
||||||
|
userEntityService = app.get<UserEntityService>(UserEntityService);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -63,7 +67,7 @@ describe('RelayService', () => {
|
||||||
expect(result.inbox).toBe('https://example.com');
|
expect(result.inbox).toBe('https://example.com');
|
||||||
expect(result.status).toBe('requesting');
|
expect(result.status).toBe('requesting');
|
||||||
expect(queueService.deliver).toHaveBeenCalled();
|
expect(queueService.deliver).toHaveBeenCalled();
|
||||||
expect(queueService.deliver.mock.lastCall![1].type).toBe('Follow');
|
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Follow');
|
||||||
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
|
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
|
||||||
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
|
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
|
||||||
});
|
});
|
||||||
|
@ -80,8 +84,8 @@ describe('RelayService', () => {
|
||||||
await relayService.removeRelay('https://example.com');
|
await relayService.removeRelay('https://example.com');
|
||||||
|
|
||||||
expect(queueService.deliver).toHaveBeenCalled();
|
expect(queueService.deliver).toHaveBeenCalled();
|
||||||
expect(queueService.deliver.mock.lastCall![1].type).toBe('Undo');
|
expect(queueService.deliver.mock.lastCall![1]?.type).toBe('Undo');
|
||||||
expect(queueService.deliver.mock.lastCall![1].object.type).toBe('Follow');
|
expect(queueService.deliver.mock.lastCall![1]?.object.type).toBe('Follow');
|
||||||
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
|
expect(queueService.deliver.mock.lastCall![2]).toBe('https://example.com');
|
||||||
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
|
//expect(queueService.deliver.mock.lastCall![0].username).toBe('relay.actor');
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div v-if="user" :class="$style.root">
|
||||||
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
||||||
{{ i18n.ts.accountMoved }}
|
{{ i18n.ts.accountMoved }}
|
||||||
<MkMention :class="$style.link" :username="username" :host="host ?? localHost"/>
|
<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -10,11 +10,17 @@
|
||||||
import MkMention from './MkMention.vue';
|
import MkMention from './MkMention.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { host as localHost } from '@/config';
|
import { host as localHost } from '@/config';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { UserLite } from 'misskey-js/built/entities';
|
||||||
|
import { api } from '@/os';
|
||||||
|
|
||||||
defineProps<{
|
const user = ref<UserLite>();
|
||||||
username: string;
|
|
||||||
host: string;
|
const props = defineProps<{
|
||||||
|
movedTo: string; // user id
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
api('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -21,6 +21,7 @@ const props = defineProps<{
|
||||||
background: var(--infoBg);
|
background: var(--infoBg);
|
||||||
color: var(--infoFg);
|
color: var(--infoFg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
background: var(--infoWarnBg);
|
background: var(--infoWarnBg);
|
||||||
|
|
|
@ -162,6 +162,7 @@ import { claimAchievement } from '@/scripts/achievements';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -255,6 +256,7 @@ useTooltip(renoteButton, async (showing) => {
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
showMovedDialog();
|
||||||
|
|
||||||
let items = [] as MenuItem[];
|
let items = [] as MenuItem[];
|
||||||
|
|
||||||
|
@ -335,6 +337,7 @@ function reply(viaKeyboard = false): void {
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
showMovedDialog();
|
||||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -401,6 +404,7 @@ async function clip() {
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
|
pleaseLogin();
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
|
|
@ -166,6 +166,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { claimAchievement } from '@/scripts/achievements';
|
import { claimAchievement } from '@/scripts/achievements';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
|
import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -248,6 +249,7 @@ useTooltip(renoteButton, async (showing) => {
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote(viaKeyboard = false) {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
showMovedDialog();
|
||||||
|
|
||||||
let items = [] as MenuItem[];
|
let items = [] as MenuItem[];
|
||||||
|
|
||||||
|
@ -318,6 +320,7 @@ function renote(viaKeyboard = false) {
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote,
|
reply: appearNote,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
|
@ -328,6 +331,7 @@ function reply(viaKeyboard = false): void {
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(viaKeyboard = false): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
|
showMovedDialog();
|
||||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: appearNote.id,
|
noteId: appearNote.id,
|
||||||
|
@ -394,6 +398,7 @@ async function clip() {
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(viaKeyboard = false): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
|
pleaseLogin();
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.unrenote,
|
text: i18n.ts.unrenote,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
|
|
@ -18,6 +18,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu';
|
||||||
import copyToClipboard from './scripts/copy-to-clipboard';
|
import copyToClipboard from './scripts/copy-to-clipboard';
|
||||||
|
import { showMovedDialog } from './scripts/show-moved-dialog';
|
||||||
|
|
||||||
export const openingWindowsCount = ref(0);
|
export const openingWindowsCount = ref(0);
|
||||||
|
|
||||||
|
@ -578,6 +579,8 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||||
|
showMovedDialog();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||||
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkFolder>
|
<MkFolder v-if="$i && !$i.movedTo">
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkFolder>
|
<MkFolder v-if="$i && !$i.movedTo">
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkFolder>
|
<MkFolder v-if="$i && !$i.movedTo">
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
<template #icon><i class="ti ti-download"></i></template>
|
<template #icon><i class="ti ti-download"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkFolder>
|
<MkFolder v-if="$i && !$i.movedTo">
|
||||||
<template #label>{{ i18n.ts.import }}</template>
|
<template #label>{{ i18n.ts.import }}</template>
|
||||||
<template #icon><i class="ti ti-upload"></i></template>
|
<template #icon><i class="ti ti-upload"></i></template>
|
||||||
<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
<MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton>
|
||||||
|
@ -97,6 +97,7 @@ import * as os from '@/os';
|
||||||
import { selectFile } from '@/scripts/select-file';
|
import { selectFile } from '@/scripts/select-file';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
const excludeMutingUsers = ref(false);
|
const excludeMutingUsers = ref(false);
|
||||||
const excludeInactiveUsers = ref(false);
|
const excludeInactiveUsers = ref(false);
|
||||||
|
|
|
@ -164,12 +164,12 @@ const menuDef = computed(() => [{
|
||||||
text: i18n.ts.importAndExport,
|
text: i18n.ts.importAndExport,
|
||||||
to: '/settings/import-export',
|
to: '/settings/import-export',
|
||||||
active: currentPage?.route.name === 'import-export',
|
active: currentPage?.route.name === 'import-export',
|
||||||
}, /*{
|
}, {
|
||||||
icon: 'ti ti-plane',
|
icon: 'ti ti-plane',
|
||||||
text: i18n.ts.accountMigration,
|
text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`,
|
||||||
to: '/settings/migration',
|
to: '/settings/migration',
|
||||||
active: currentPage?.route.name === 'migration',
|
active: currentPage?.route.name === 'migration',
|
||||||
},*/ {
|
}, {
|
||||||
icon: 'ti ti-dots',
|
icon: 'ti ti-dots',
|
||||||
text: i18n.ts.other,
|
text: i18n.ts.other,
|
||||||
to: '/settings/other',
|
to: '/settings/other',
|
||||||
|
|
|
@ -1,63 +1,121 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<FormSection first>
|
<FormInfo warn>
|
||||||
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
|
{{ i18n.ts.ThisIsExperimentalFeature }}
|
||||||
<MkInput v-model="moveToAccount" manual-save>
|
</FormInfo>
|
||||||
<template #prefix><i class="ti ti-plane-departure"></i></template>
|
<MkFolder :default-open="true">
|
||||||
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
|
<template #icon><i class="ti ti-plane-arrival"></i></template>
|
||||||
</MkInput>
|
|
||||||
</FormSection>
|
|
||||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
|
|
||||||
|
|
||||||
<FormSection>
|
|
||||||
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
|
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
|
||||||
<MkInput v-model="accountAlias" manual-save>
|
<template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template>
|
||||||
<template #prefix><i class="ti ti-plane-arrival"></i></template>
|
|
||||||
<template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template>
|
<div class="_gaps_m">
|
||||||
</MkInput>
|
<FormInfo warn>
|
||||||
</FormSection>
|
{{ i18n.ts._accountMigration.moveFromDescription }}
|
||||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo>
|
</FormInfo>
|
||||||
|
<div>
|
||||||
|
<MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]">
|
||||||
|
<template #prefix><i class="ti ti-plane-arrival"></i></template>
|
||||||
|
<template #label>{{ i18n.t('_accountMigration.moveFromLabel', { n: i + 1 }) }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder :default-open="!!$i?.movedTo">
|
||||||
|
<template #icon><i class="ti ti-plane-departure"></i></template>
|
||||||
|
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<FormInfo>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
|
||||||
|
|
||||||
|
<template v-if="$i && !$i.movedTo">
|
||||||
|
<FormInfo>{{ i18n.ts._accountMigration.moveAccountHowTo }}</FormInfo>
|
||||||
|
<FormInfo warn>{{ i18n.ts._accountMigration.moveCannotBeUndone }}</FormInfo>
|
||||||
|
|
||||||
|
<MkInput v-model="moveToAccount">
|
||||||
|
<template #prefix><i class="ti ti-plane-departure"></i></template>
|
||||||
|
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkButton inline danger :disabled="!moveToAccount" @click="move">
|
||||||
|
<i class="ti ti-check"></i> {{ i18n.ts._accountMigration.startMigration }}
|
||||||
|
</MkButton>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="$i">
|
||||||
|
<FormInfo>{{ i18n.ts._accountMigration.postMigrationNote }}</FormInfo>
|
||||||
|
<FormInfo warn>{{ i18n.ts._accountMigration.movedAndCannotBeUndone }}</FormInfo>
|
||||||
|
<div>{{ i18n.ts._accountMigration.movedTo }}</div>
|
||||||
|
<MkUserInfo v-if="movedTo" :user="movedTo" class="_panel _shadow" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref } from 'vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
|
||||||
import FormInfo from '@/components/MkInfo.vue';
|
import FormInfo from '@/components/MkInfo.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { toString } from 'misskey-js/built/acct';
|
||||||
|
import { UserDetailed } from 'misskey-js/built/entities';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
const moveToAccount = ref('');
|
const moveToAccount = ref('');
|
||||||
const accountAlias = ref('');
|
const movedTo = ref<UserDetailed>();
|
||||||
|
const accountAliases = ref(['']);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if ($i?.movedTo) {
|
||||||
|
movedTo.value = await os.api('users/show', { userId: $i.movedTo });
|
||||||
|
} else {
|
||||||
|
moveToAccount.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($i?.alsoKnownAs && $i.alsoKnownAs.length > 0) {
|
||||||
|
const alsoKnownAs = await os.api('users/show', { userIds: $i.alsoKnownAs });
|
||||||
|
accountAliases.value = (alsoKnownAs && alsoKnownAs.length > 0) ? alsoKnownAs.map(user => `@${toString(user)}`) : [''];
|
||||||
|
} else {
|
||||||
|
accountAliases.value = [''];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function move(): Promise<void> {
|
async function move(): Promise<void> {
|
||||||
const account = moveToAccount.value;
|
const account = moveToAccount.value;
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.t('migrationConfirm', { account: account.toString() }),
|
text: i18n.t('_accountMigration.migrationConfirm', { account }),
|
||||||
});
|
});
|
||||||
if (confirm.canceled) return;
|
if (confirm.canceled) return;
|
||||||
os.apiWithDialog('i/move', {
|
await os.apiWithDialog('i/move', {
|
||||||
moveToAccount: account,
|
moveToAccount: account,
|
||||||
});
|
});
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(): void {
|
||||||
|
accountAliases.value.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
async function save(): Promise<void> {
|
||||||
const account = accountAlias.value;
|
const alsoKnownAs = accountAliases.value.map(alias => alias.trim()).filter(alias => alias !== '');
|
||||||
os.apiWithDialog('i/known-as', {
|
const i = await os.apiWithDialog('i/update', {
|
||||||
alsoKnownAs: account,
|
alsoKnownAs,
|
||||||
});
|
});
|
||||||
|
$i.alsoKnownAs = i.alsoKnownAs;
|
||||||
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(accountAlias, async () => {
|
init();
|
||||||
await save();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(moveToAccount, async () => {
|
|
||||||
await move();
|
|
||||||
});
|
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.accountMigration,
|
title: i18n.ts.accountMigration,
|
||||||
|
|
|
@ -38,6 +38,10 @@
|
||||||
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
|
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
|
<div>
|
||||||
|
<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
</div>
|
||||||
<FormSplit v-for="(record, i) in fields" :min-width="250">
|
<FormSplit v-for="(record, i) in fields" :min-width="250">
|
||||||
<MkInput v-model="record.name" small>
|
<MkInput v-model="record.name" small>
|
||||||
<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
|
<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
|
||||||
|
@ -46,10 +50,6 @@
|
||||||
<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
|
<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
<div>
|
|
||||||
<MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
|
||||||
<MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
||||||
|
|
||||||
<div class="profile _gaps">
|
<div class="profile _gaps">
|
||||||
<MkAccountMoved v-if="user.movedToUri" :host="user.movedToUri.host" :username="user.movedToUri.username"/>
|
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo" />
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
||||||
|
|
||||||
<div :key="user.id" class="main _panel">
|
<div :key="user.id" class="main _panel">
|
||||||
|
|
|
@ -464,6 +464,7 @@ const claimingQueue = new Set<string>();
|
||||||
|
|
||||||
export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
|
export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
|
||||||
if ($i == null) return;
|
if ($i == null) return;
|
||||||
|
if ($i.movedTo) return;
|
||||||
if (claimedAchievements.includes(type)) return;
|
if (claimedAchievements.includes(type)) return;
|
||||||
claimingQueue.add(type);
|
claimingQueue.add(type);
|
||||||
claimedAchievements.push(type);
|
claimedAchievements.push(type);
|
||||||
|
|
|
@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) {
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
|
|
||||||
if (!path) throw new Error('signin required');
|
throw new Error('signin required');
|
||||||
}
|
}
|
||||||
|
|
16
packages/frontend/src/scripts/show-moved-dialog.ts
Normal file
16
packages/frontend/src/scripts/show-moved-dialog.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
export function showMovedDialog() {
|
||||||
|
if (!$i) return;
|
||||||
|
if (!$i.movedTo) return;
|
||||||
|
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.accountMovedShort,
|
||||||
|
text: i18n.ts.operationForbidden,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error('account moved');
|
||||||
|
}
|
|
@ -1357,10 +1357,6 @@ export type Endpoints = {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
'i/known-as': {
|
|
||||||
req: TODO;
|
|
||||||
res: TODO;
|
|
||||||
};
|
|
||||||
'i/notifications': {
|
'i/notifications': {
|
||||||
req: {
|
req: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
@ -1511,6 +1507,7 @@ export type Endpoints = {
|
||||||
mutedWords?: string[][];
|
mutedWords?: string[][];
|
||||||
mutingNotificationTypes?: Notification_2['type'][];
|
mutingNotificationTypes?: Notification_2['type'][];
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
|
alsoKnownAs?: string[];
|
||||||
};
|
};
|
||||||
res: MeDetailed;
|
res: MeDetailed;
|
||||||
};
|
};
|
||||||
|
@ -2634,6 +2631,7 @@ type User = UserLite | UserDetailed;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type UserDetailed = UserLite & {
|
type UserDetailed = UserLite & {
|
||||||
|
alsoKnownAs: string[];
|
||||||
bannerBlurhash: string | null;
|
bannerBlurhash: string | null;
|
||||||
bannerColor: string | null;
|
bannerColor: string | null;
|
||||||
bannerUrl: string | null;
|
bannerUrl: string | null;
|
||||||
|
@ -2664,6 +2662,7 @@ type UserDetailed = UserLite & {
|
||||||
lang: string | null;
|
lang: string | null;
|
||||||
lastFetchedAt?: DateString;
|
lastFetchedAt?: DateString;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
|
movedTo: string;
|
||||||
notesCount: number;
|
notesCount: number;
|
||||||
pinnedNoteIds: ID[];
|
pinnedNoteIds: ID[];
|
||||||
pinnedNotes: Note[];
|
pinnedNotes: Note[];
|
||||||
|
@ -2697,8 +2696,6 @@ type UserLite = {
|
||||||
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
avatarBlurhash: string;
|
avatarBlurhash: string;
|
||||||
alsoKnownAs: string[];
|
|
||||||
movedToUri: any;
|
|
||||||
emojis: {
|
emojis: {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -363,7 +363,6 @@ export type Endpoints = {
|
||||||
'i/import-following': { req: TODO; res: TODO; };
|
'i/import-following': { req: TODO; res: TODO; };
|
||||||
'i/import-user-lists': { req: TODO; res: TODO; };
|
'i/import-user-lists': { req: TODO; res: TODO; };
|
||||||
'i/move': { req: TODO; res: TODO; };
|
'i/move': { req: TODO; res: TODO; };
|
||||||
'i/known-as': { req: TODO; res: TODO; };
|
|
||||||
'i/notifications': { req: {
|
'i/notifications': { req: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
sinceId?: Notification['id'];
|
sinceId?: Notification['id'];
|
||||||
|
@ -421,6 +420,7 @@ export type Endpoints = {
|
||||||
mutedWords?: string[][];
|
mutedWords?: string[][];
|
||||||
mutingNotificationTypes?: Notification['type'][];
|
mutingNotificationTypes?: Notification['type'][];
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
|
alsoKnownAs?: string[];
|
||||||
}; res: MeDetailed; };
|
}; res: MeDetailed; };
|
||||||
'i/user-group-invites': { req: TODO; res: TODO; };
|
'i/user-group-invites': { req: TODO; res: TODO; };
|
||||||
'i/2fa/done': { req: TODO; res: TODO; };
|
'i/2fa/done': { req: TODO; res: TODO; };
|
||||||
|
|
|
@ -14,8 +14,6 @@ export type UserLite = {
|
||||||
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
onlineStatus: 'online' | 'active' | 'offline' | 'unknown';
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
avatarBlurhash: string;
|
avatarBlurhash: string;
|
||||||
alsoKnownAs: string[];
|
|
||||||
movedToUri: any;
|
|
||||||
emojis: {
|
emojis: {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -31,6 +29,7 @@ export type UserLite = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserDetailed = UserLite & {
|
export type UserDetailed = UserLite & {
|
||||||
|
alsoKnownAs: string[];
|
||||||
bannerBlurhash: string | null;
|
bannerBlurhash: string | null;
|
||||||
bannerColor: string | null;
|
bannerColor: string | null;
|
||||||
bannerUrl: string | null;
|
bannerUrl: string | null;
|
||||||
|
@ -58,6 +57,7 @@ export type UserDetailed = UserLite & {
|
||||||
lang: string | null;
|
lang: string | null;
|
||||||
lastFetchedAt?: DateString;
|
lastFetchedAt?: DateString;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
|
movedTo: string;
|
||||||
notesCount: number;
|
notesCount: number;
|
||||||
pinnedNoteIds: ID[];
|
pinnedNoteIds: ID[];
|
||||||
pinnedNotes: Note[];
|
pinnedNotes: Note[];
|
||||||
|
|
Loading…
Reference in a new issue