enhance(federation): refresh followings and remove all followings

This commit is contained in:
オスカー、 2024-08-08 23:50:41 +09:00
parent 9cf3e8703d
commit f45f08fa48
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
6 changed files with 75 additions and 18 deletions

12
locales/index.d.ts vendored
View File

@ -5135,6 +5135,18 @@ export interface Locale extends ILocale {
* *
*/ */
"onboarding": string; "onboarding": string;
/**
*
*/
"refreshMetadata": string;
/**
*
*/
"removeAllFollowings": string;
/**
* {host}
*/
"areYouSureToRemoveAllFollowings": ParameterizedString<"host">;
"_bubbleGame": { "_bubbleGame": {
/** /**
* *

View File

@ -1279,6 +1279,9 @@ sensitiveDoubleClickRequired: "敏感な内容のメディアをダブルクリ
prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする" prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする"
prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。" prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。"
onboarding: "オンボーディング" onboarding: "オンボーディング"
refreshMetadata: "サーバー情報を更新"
removeAllFollowings: "相互フォロー解除"
areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"

View File

@ -1261,9 +1261,12 @@ hideSensitiveInformation: "민감한 정보 숨기기"
youAreHidingSensitiveInformation: "'프라이빗 모드'에 의해 숨겨졌습니다." youAreHidingSensitiveInformation: "'프라이빗 모드'에 의해 숨겨졌습니다."
temporarilySeeThis: "무시하고 표시하기" temporarilySeeThis: "무시하고 표시하기"
sensitiveDoubleClickRequired: "민감한 내용의 미디어를 더블 클릭해서 표시" sensitiveDoubleClickRequired: "민감한 내용의 미디어를 더블 클릭해서 표시"
prohibitSkippingInitialTutorial: "チュートリアルをスキップできないようにする" prohibitSkippingInitialTutorial: "튜토리얼을 건너뛸 수 없도록 하기"
prohibitSkippingInitialTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了しなかったりチュートリアルページを回避したりした場合でも、強制的にリダイレクトされます。" prohibitSkippingInitialTutorialDescription: "신규 가입한 사용자에게 표시되는 튜토리얼을 건너뛸 수 없도록 합니다. 튜토리얼을 완료하지 않거나 튜토리얼 페이지를 우회하는 경우에도 강제로 리디렉션됩니다."
onboarding: "온보딩" onboarding: "온보딩"
refreshMetadata: "서버 정보를 갱신하기"
removeAllFollowings: "모든 팔로우 관계를 제거하기"
areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요."
_bubbleGame: _bubbleGame:
howToPlay: "설명" howToPlay: "설명"
hold: "홀드" hold: "홀드"

View File

@ -4,18 +4,28 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { InstancesRepository } from '@/models/_.js'; import type { FollowingsRepository, InstancesRepository } from '@/models/_.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireModerator: true, requireAdmin: true,
kind: 'write:admin:federation', kind: 'write:admin:federation',
errors: {
instanceNotFound: {
message: 'Instance with that hostname is not found.',
code: 'INSTANCE_NOT_FOUND',
id: '82791415-ae4b-4e82-bffe-e3dbc4322a0a',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -31,18 +41,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private followingsRepository: FollowingsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService, private fetchInstanceMetadataService: FetchInstanceMetadataService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) }); const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) });
if (instance == null) { if (instance == null) {
throw new Error('instance not found'); throw new ApiError(meta.errors.instanceNotFound);
} }
this.fetchInstanceMetadataService.fetchInstanceMetadata(instance, true); const followingCount = await this.followingsRepository.countBy({ followerHost: this.utilityService.toPuny(ps.host) });
const followersCount = await this.followingsRepository.countBy({ followeeHost: this.utilityService.toPuny(ps.host) });
await this.federatedInstanceService.update(instance.id, {
followingCount: followingCount,
followersCount: followersCount,
});
await this.fetchInstanceMetadataService.fetchInstanceMetadata(instance, true);
}); });
} }
} }

View File

@ -13,7 +13,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireModerator: true, requireAdmin: true,
kind: 'write:admin:federation', kind: 'write:admin:federation',
} as const; } as const;
@ -40,13 +40,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const followings = await this.followingsRepository.findBy({ const followings = await this.followingsRepository.findBy({
followerHost: ps.host, followerHost: ps.host,
}); });
const followers = await this.followingsRepository.findBy({
followeeHost: ps.host,
});
const pairs = await Promise.all(followings.map(f => Promise.all([ const followingPairs = await Promise.all(followings.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }),
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
const followerPairs = await Promise.all(followers.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }), this.usersRepository.findOneByOrFail({ id: f.followerId }),
this.usersRepository.findOneByOrFail({ id: f.followeeId }), this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }]))); ]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true }))); await this.queueService.createUnfollowJob(followingPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
await this.queueService.createUnfollowJob(followerPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
}); });
} }
} }

View File

@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value>{{ instance.description }}</template> <template #value>{{ instance.description }}</template>
</MkKeyValue> </MkKeyValue>
<FormSection v-if="iAmModerator"> <MkFolder v-if="iAmModerator">
<template #label>Moderation</template> <template #label>{{ i18n.ts.moderation }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave> <MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template> <template #label>{{ i18n.ts.moderationNote }}</template>
@ -42,9 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkSwitch v-model="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch> <MkSwitch v-model="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkButton :disabled="!meta || !instance" @click="refreshMetadata"><i class="ti ti-refresh"></i> {{ i18n.ts.refreshMetadata }}</MkButton>
<MkButton :disabled="!meta || !instance" @click="removeAllFollowings"><i class="ti ti-users-minus"></i> {{ i18n.ts.removeAllFollowings }}</MkButton>
</div> </div>
</FormSection> </MkFolder>
<FormSection> <FormSection>
<MkKeyValue oneline style="margin: 1em 0;"> <MkKeyValue oneline style="margin: 1em 0;">
@ -134,6 +135,7 @@ import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
@ -235,13 +237,23 @@ async function toggleSuspend(): Promise<void> {
}); });
} }
function refreshMetadata(): void { async function refreshMetadata(): Promise<void> {
if (!instance.value) throw new Error('No instance?'); if (!instance.value) throw new Error('No instance?');
misskeyApi('admin/federation/refresh-remote-instance-metadata', { await os.apiWithDialog('admin/federation/refresh-remote-instance-metadata', {
host: instance.value.host, host: instance.value.host,
}); });
os.alert({ }
text: 'Refresh requested',
async function removeAllFollowings(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.areYouSureToRemoveAllFollowings({ host: instance.value.host }),
});
if (canceled) return;
await os.apiWithDialog('admin/federation/remove-all-following', {
host: instance.value.host,
}); });
} }