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
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div :class="$style.root">
|
||||
<div v-if="user" :class="$style.root">
|
||||
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
||||
{{ i18n.ts.accountMoved }}
|
||||
<MkMention :class="$style.link" :username="username" :host="host ?? localHost"/>
|
||||
<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -10,11 +10,17 @@
|
|||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { host as localHost } from '@/config';
|
||||
import { ref } from 'vue';
|
||||
import { UserLite } from 'misskey-js/built/entities';
|
||||
import { api } from '@/os';
|
||||
|
||||
defineProps<{
|
||||
username: string;
|
||||
host: string;
|
||||
const user = ref<UserLite>();
|
||||
|
||||
const props = defineProps<{
|
||||
movedTo: string; // user id
|
||||
}>();
|
||||
|
||||
api('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -21,6 +21,7 @@ const props = defineProps<{
|
|||
background: var(--infoBg);
|
||||
color: var(--infoFg);
|
||||
border-radius: var(--radius);
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.warn {
|
||||
background: var(--infoWarnBg);
|
||||
|
|
|
@ -162,6 +162,7 @@ import { claimAchievement } from '@/scripts/achievements';
|
|||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
|
@ -255,6 +256,7 @@ useTooltip(renoteButton, async (showing) => {
|
|||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
|
||||
let items = [] as MenuItem[];
|
||||
|
||||
|
@ -335,6 +337,7 @@ function reply(viaKeyboard = false): void {
|
|||
|
||||
function react(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
|
@ -401,6 +404,7 @@ async function clip() {
|
|||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
if (!isMyRenote) return;
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
@ -166,6 +166,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
|
|||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
|
@ -248,6 +249,7 @@ useTooltip(renoteButton, async (showing) => {
|
|||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
|
||||
let items = [] as MenuItem[];
|
||||
|
||||
|
@ -318,6 +320,7 @@ function renote(viaKeyboard = false) {
|
|||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
os.post({
|
||||
reply: appearNote,
|
||||
animation: !viaKeyboard,
|
||||
|
@ -328,6 +331,7 @@ function reply(viaKeyboard = false): void {
|
|||
|
||||
function react(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
if (appearNote.reactionAcceptance === 'likeOnly') {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
|
@ -394,6 +398,7 @@ async function clip() {
|
|||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
if (!isMyRenote) return;
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
@ -18,6 +18,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
|||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import copyToClipboard from './scripts/copy-to-clipboard';
|
||||
import { showMovedDialog } from './scripts/show-moved-dialog';
|
||||
|
||||
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> {
|
||||
showMovedDialog();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
// 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>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkFolder>
|
||||
<MkFolder v-if="$i && !$i.movedTo">
|
||||
<template #label>{{ i18n.ts.import }}</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>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<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>
|
||||
</MkFolder>
|
||||
<MkFolder>
|
||||
<MkFolder v-if="$i && !$i.movedTo">
|
||||
<template #label>{{ i18n.ts.import }}</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>
|
||||
|
@ -62,7 +62,7 @@
|
|||
<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>
|
||||
</MkFolder>
|
||||
<MkFolder>
|
||||
<MkFolder v-if="$i && !$i.movedTo">
|
||||
<template #label>{{ i18n.ts.import }}</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>
|
||||
|
@ -77,7 +77,7 @@
|
|||
<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>
|
||||
</MkFolder>
|
||||
<MkFolder>
|
||||
<MkFolder v-if="$i && !$i.movedTo">
|
||||
<template #label>{{ i18n.ts.import }}</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>
|
||||
|
@ -97,6 +97,7 @@ import * as os from '@/os';
|
|||
import { selectFile } from '@/scripts/select-file';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const excludeMutingUsers = ref(false);
|
||||
const excludeInactiveUsers = ref(false);
|
||||
|
|
|
@ -164,12 +164,12 @@ const menuDef = computed(() => [{
|
|||
text: i18n.ts.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
active: currentPage?.route.name === 'import-export',
|
||||
}, /*{
|
||||
}, {
|
||||
icon: 'ti ti-plane',
|
||||
text: i18n.ts.accountMigration,
|
||||
text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`,
|
||||
to: '/settings/migration',
|
||||
active: currentPage?.route.name === 'migration',
|
||||
},*/ {
|
||||
}, {
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.other,
|
||||
to: '/settings/other',
|
||||
|
|
|
@ -1,63 +1,121 @@
|
|||
<template>
|
||||
<div class="_gaps_m">
|
||||
<FormSection first>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
|
||||
<MkInput v-model="moveToAccount" manual-save>
|
||||
<template #prefix><i class="ti ti-plane-departure"></i></template>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
|
||||
</MkInput>
|
||||
</FormSection>
|
||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
|
||||
|
||||
<FormSection>
|
||||
<FormInfo warn>
|
||||
{{ i18n.ts.ThisIsExperimentalFeature }}
|
||||
</FormInfo>
|
||||
<MkFolder :default-open="true">
|
||||
<template #icon><i class="ti ti-plane-arrival"></i></template>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
|
||||
<MkInput v-model="accountAlias" manual-save>
|
||||
<template #prefix><i class="ti ti-plane-arrival"></i></template>
|
||||
<template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template>
|
||||
</MkInput>
|
||||
</FormSection>
|
||||
<FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo>
|
||||
<template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<FormInfo warn>
|
||||
{{ i18n.ts._accountMigration.moveFromDescription }}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { ref } from 'vue';
|
||||
import FormInfo from '@/components/MkInfo.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 { i18n } from '@/i18n';
|
||||
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 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> {
|
||||
const account = moveToAccount.value;
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('migrationConfirm', { account: account.toString() }),
|
||||
text: i18n.t('_accountMigration.migrationConfirm', { account }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('i/move', {
|
||||
await os.apiWithDialog('i/move', {
|
||||
moveToAccount: account,
|
||||
});
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
accountAliases.value.push('');
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
const account = accountAlias.value;
|
||||
os.apiWithDialog('i/known-as', {
|
||||
alsoKnownAs: account,
|
||||
const alsoKnownAs = accountAliases.value.map(alias => alias.trim()).filter(alias => alias !== '');
|
||||
const i = await os.apiWithDialog('i/update', {
|
||||
alsoKnownAs,
|
||||
});
|
||||
$i.alsoKnownAs = i.alsoKnownAs;
|
||||
init();
|
||||
}
|
||||
|
||||
watch(accountAlias, async () => {
|
||||
await save();
|
||||
});
|
||||
|
||||
watch(moveToAccount, async () => {
|
||||
await move();
|
||||
});
|
||||
init();
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.accountMigration,
|
||||
|
|
|
@ -38,6 +38,10 @@
|
|||
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
|
||||
|
||||
<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">
|
||||
<MkInput v-model="record.name" small>
|
||||
<template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template>
|
||||
|
@ -46,10 +50,6 @@
|
|||
<template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template>
|
||||
</MkInput>
|
||||
</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>
|
||||
</MkFolder>
|
||||
<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="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"/>
|
||||
|
||||
<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]) {
|
||||
if ($i == null) return;
|
||||
if ($i.movedTo) return;
|
||||
if (claimedAchievements.includes(type)) return;
|
||||
claimingQueue.add(type);
|
||||
claimedAchievements.push(type);
|
||||
|
|
|
@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) {
|
|||
},
|
||||
}, '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');
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue