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
|
@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.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 type { Following } from '@/models/entities/Following.js';
|
||||
import { countIf } from '@/misc/prelude/array.js';
|
||||
|
@ -630,7 +630,7 @@ export class ActivityPubServerService {
|
|||
id: request.params.followee,
|
||||
host: Not(IsNull()),
|
||||
}),
|
||||
]);
|
||||
]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
|
||||
|
||||
if (follower == null || followee == null) {
|
||||
reply.code(404);
|
||||
|
@ -665,7 +665,7 @@ export class ActivityPubServerService {
|
|||
id: followRequest.followeeId,
|
||||
host: Not(IsNull()),
|
||||
}),
|
||||
]);
|
||||
]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
|
||||
|
||||
if (follower == null || followee == null) {
|
||||
reply.code(404);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
|
|||
import type { User } from '@/models/entities/User.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
@ -23,6 +24,7 @@ export class WellKnownServerService {
|
|||
private usersRepository: UsersRepository,
|
||||
|
||||
private nodeinfoServerService: NodeinfoServerService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
//this.createServer = this.createServer.bind(this);
|
||||
}
|
||||
|
@ -130,7 +132,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
|||
const self = {
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${this.config.url}/users/${user.id}`,
|
||||
href: this.userEntityService.genLocalUserUri(user.id),
|
||||
};
|
||||
const profilePage = {
|
||||
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) {
|
||||
const myRoles = await this.roleService.getUserRoles(user!.id);
|
||||
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_update from './endpoints/i/update.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_show from './endpoints/i/webhooks/show.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_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_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_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 };
|
||||
|
@ -901,7 +899,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_updateEmail,
|
||||
$i_update,
|
||||
$i_move,
|
||||
$i_knownAs,
|
||||
$i_webhooks_create,
|
||||
$i_webhooks_list,
|
||||
$i_webhooks_show,
|
||||
|
@ -1236,7 +1233,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_updateEmail,
|
||||
$i_update,
|
||||
$i_move,
|
||||
$i_knownAs,
|
||||
$i_webhooks_create,
|
||||
$i_webhooks_list,
|
||||
$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_update from './endpoints/i/update.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_show from './endpoints/i/webhooks/show.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/update-email', ep___i_updateEmail],
|
||||
['i/update', ep___i_update],
|
||||
//['i/move', ep___i_move],
|
||||
//['i/known-as', ep___i_knownAs],
|
||||
['i/move', ep___i_move],
|
||||
['i/webhooks/create', ep___i_webhooks_create],
|
||||
['i/webhooks/list', ep___i_webhooks_list],
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
|
@ -704,6 +702,12 @@ export interface IEndpointMeta {
|
|||
|
||||
readonly requireRolePolicy?: keyof RolePolicies;
|
||||
|
||||
/**
|
||||
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
||||
* 省略した場合は false として解釈されます。
|
||||
*/
|
||||
readonly prohibitMoved?: boolean;
|
||||
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
@ -71,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
|
||||
private antennaEntityService: AntennaEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:channels',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
res: {
|
||||
|
@ -57,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
|
||||
throw new ApiError(meta.errors.tooManyClips);
|
||||
}
|
||||
|
||||
|
||||
const clip = await this.clipsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:clip-favorite',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:clip-favorite',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -15,6 +15,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 120,
|
||||
|
|
|
@ -19,6 +19,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:drive',
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:flash',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:flash-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:flash-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:flash',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -19,6 +19,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:following',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:gallery-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:gallery-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService
|
|||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
|
@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 1,
|
||||
|
@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
|
@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
|
|||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 1,
|
||||
|
@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
//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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 { ApiError } from '@/server/api/error.js';
|
||||
|
||||
import { LocalUser, RemoteUser } from '@/models/entities/User.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';
|
||||
import { GetterService } from '@/server/api/GetterService.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 = {
|
||||
tags: ['users'],
|
||||
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 5,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMoveTarget: {
|
||||
message: 'No such move target.',
|
||||
code: 'NO_SUCH_MOVE_TARGET',
|
||||
id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
|
||||
},
|
||||
remoteAccountForbids: {
|
||||
destinationAccountForbids: {
|
||||
message:
|
||||
'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
|
||||
code: 'REMOTE_ACCOUNT_FORBIDS',
|
||||
'Destination account doesn\'t have proper \'Known As\' alias, or has already moved.',
|
||||
code: 'DESTINATION_ACCOUNT_FORBIDS',
|
||||
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: {
|
||||
message: 'The root can\'t migrate.',
|
||||
code: 'NOT_ROOT_FORBIDDEN',
|
||||
|
@ -84,57 +79,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private getterService: GetterService,
|
||||
private apPersonService: ApPersonService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// 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
|
||||
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
|
||||
// abort if user has already moved
|
||||
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
|
||||
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('@');
|
||||
const { username, host } = Acct.parse(ps.moveToAccount);
|
||||
// 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}`);
|
||||
throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
});
|
||||
const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id);
|
||||
if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull);
|
||||
const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser;
|
||||
const newUri = this.userEntityService.getUserUri(destination);
|
||||
|
||||
// update local db
|
||||
await this.apPersonService.updatePerson(remoteMoveTo.uri);
|
||||
await this.apPersonService.updatePerson(newUri);
|
||||
// retrieve updated user
|
||||
moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri);
|
||||
// only allow moving to a remote account
|
||||
if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote);
|
||||
moveTo = await this.apPersonService.resolvePerson(newUri);
|
||||
|
||||
let allowed = false;
|
||||
|
||||
const fromUrl = `${this.config.url}/users/${me.id}`;
|
||||
// make sure that the user has indicated the old account as an alias
|
||||
moveTo.alsoKnownAs?.forEach((elem) => {
|
||||
if (fromUrl.includes(elem)) allowed = true;
|
||||
});
|
||||
const fromUrl = this.userEntityService.genLocalUserUri(me.id);
|
||||
let allowed = false;
|
||||
if (moveTo.alsoKnownAs) {
|
||||
for (const knownAs of moveTo.alsoKnownAs) {
|
||||
if (knownAs.includes(fromUrl)) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.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 { User } 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 { RoleService } from '@/core/RoleService.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 { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -71,6 +75,24 @@ export const meta = {
|
|||
code: 'TOO_MANY_MUTED_WORDS',
|
||||
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: {
|
||||
|
@ -129,6 +151,12 @@ export const paramDef = {
|
|||
emailNotificationTypes: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
alsoKnownAs: {
|
||||
type: 'array',
|
||||
maxItems: 10,
|
||||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -153,6 +181,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
private globalEventService: GlobalEventService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private accountUpdateService: AccountUpdateService,
|
||||
private accountMoveService: AccountMoveService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apiLoggerService: ApiLoggerService,
|
||||
private hashtagService: HashtagService,
|
||||
private roleService: RoleService,
|
||||
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
|
||||
|
||||
let emojis = [] as string[];
|
||||
|
@ -287,6 +350,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
//#endregion
|
||||
|
||||
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);
|
||||
|
||||
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const meta = {
|
|||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:mutes',
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 300,
|
||||
|
|
|
@ -12,6 +12,7 @@ export const meta = {
|
|||
tags: ['notes', 'favorites'],
|
||||
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:favorites',
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:votes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:reactions',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:pages',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:page-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -9,6 +9,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:page-likes',
|
||||
|
||||
errors: {
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:pages',
|
||||
|
||||
limit: {
|
||||
|
|
|
@ -13,6 +13,7 @@ export const meta = {
|
|||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:mutes',
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
description: 'Create a new list of users.',
|
||||
|
@ -58,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
throw new ApiError(meta.errors.tooManyUserLists);
|
||||
}
|
||||
|
||||
|
||||
const userList = await this.userListsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
description: 'Remove a user from a list.',
|
||||
|
|
|
@ -12,6 +12,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
description: 'Add a user to an existing list.',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue