Skip to content

Commit 83a9aa4

Browse files
anatawa12syuilo
andauthored
feat: suspend instance improvements (misskey-dev#13861)
* feat(backend): dead instance detection * feat(backend): suspend type detection * feat(frontend): show suspend reason on frontend * feat(backend): resume federation automatically if the server is automatically suspended * docs(changelog): 配信停止まわりの改善 * lint: fix lint errors * Update packages/frontend/src/pages/instance-info.vue * lint: fix lint error * chore: suspendedState => suspensionState --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
1 parent 611e303 commit 83a9aa4

File tree

15 files changed

+193
-17
lines changed

15 files changed

+193
-17
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
- サスペンド済みユーザーか
1616
- 鍵アカウントユーザーか
1717
- 「アカウントを見つけやすくする」が有効なユーザーか
18+
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
19+
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
20+
- Enhance: 配信停止の理由を表示するように
1821
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
1922
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
2023
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正

locales/index.d.ts

+32
Original file line numberDiff line numberDiff line change
@@ -4972,6 +4972,38 @@ export interface Locale extends ILocale {
49724972
* お問い合わせ
49734973
*/
49744974
"inquiry": string;
4975+
"_delivery": {
4976+
/**
4977+
* 配信状態
4978+
*/
4979+
"status": string;
4980+
/**
4981+
* 配信停止
4982+
*/
4983+
"stop": string;
4984+
/**
4985+
* 配信再開
4986+
*/
4987+
"resume": string;
4988+
"_type": {
4989+
/**
4990+
* 配信中
4991+
*/
4992+
"none": string;
4993+
/**
4994+
* 手動停止中
4995+
*/
4996+
"manuallySuspended": string;
4997+
/**
4998+
* サーバー削除のため停止中
4999+
*/
5000+
"goneSuspended": string;
5001+
/**
5002+
* サーバー応答なしのため停止中
5003+
*/
5004+
"autoSuspendedForNotResponding": string;
5005+
};
5006+
};
49755007
"_bubbleGame": {
49765008
/**
49775009
* 遊び方

locales/ja-JP.yml

+10
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,16 @@ noDescription: "説明文はありません"
12401240
alwaysConfirmFollow: "フォローの際常に確認する"
12411241
inquiry: "お問い合わせ"
12421242

1243+
_delivery:
1244+
status: "配信状態"
1245+
stop: "配信停止"
1246+
resume: "配信再開"
1247+
_type:
1248+
none: "配信中"
1249+
manuallySuspended: "手動停止中"
1250+
goneSuspended: "サーバー削除のため停止中"
1251+
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
1252+
12431253
_bubbleGame:
12441254
howToPlay: "遊び方"
12451255
hold: "ホールド"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
export class NotRespondingSince1716345015347 {
7+
name = 'NotRespondingSince1716345015347'
8+
9+
async up(queryRunner) {
10+
await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
11+
}
12+
13+
async down(queryRunner) {
14+
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
export class SuspensionStateInsteadOfIsSspended1716345771510 {
7+
name = 'SuspensionStateInsteadOfIsSspended1716345771510'
8+
9+
async up(queryRunner) {
10+
await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
11+
12+
await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
13+
14+
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
15+
16+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
17+
18+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
19+
CASE "suspensionState"
20+
WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
21+
ELSE 'none'::instance_suspensionstate_enum
22+
END
23+
)`);
24+
25+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
26+
27+
await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
28+
}
29+
30+
async down(queryRunner) {
31+
await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
32+
33+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
34+
35+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
36+
CASE "suspensionState"
37+
WHEN 'none'::instance_suspensionstate_enum THEN FALSE
38+
ELSE TRUE
39+
END
40+
)`);
41+
42+
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
43+
44+
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
45+
46+
await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
47+
48+
await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
49+
}
50+
}

packages/backend/src/core/entities/InstanceEntityService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export class InstanceEntityService {
3939
followingCount: instance.followingCount,
4040
followersCount: instance.followersCount,
4141
isNotResponding: instance.isNotResponding,
42-
isSuspended: instance.isSuspended,
42+
isSuspended: instance.suspensionState !== 'none',
43+
suspensionState: instance.suspensionState,
4344
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
4445
softwareName: instance.softwareName,
4546
softwareVersion: instance.softwareVersion,

packages/backend/src/models/Instance.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,22 @@ export class MiInstance {
8181
public isNotResponding: boolean;
8282

8383
/**
84-
* このインスタンスへの配信を停止するか
84+
* このインスタンスと不通になった日時
85+
*/
86+
@Column('timestamp with time zone', {
87+
nullable: true,
88+
})
89+
public notRespondingSince: Date | null;
90+
91+
/**
92+
* このインスタンスへの配信状態
8593
*/
8694
@Index()
87-
@Column('boolean', {
88-
default: false,
95+
@Column('enum', {
96+
default: 'none',
97+
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
8998
})
90-
public isSuspended: boolean;
99+
public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
91100

92101
@Column('varchar', {
93102
length: 64, nullable: true,

packages/backend/src/models/json-schema/federation-instance.ts

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
4545
type: 'boolean',
4646
optional: false, nullable: false,
4747
},
48+
suspensionState: {
49+
type: 'string',
50+
nullable: false, optional: false,
51+
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
52+
},
4853
isBlocked: {
4954
type: 'boolean',
5055
optional: false, nullable: false,

packages/backend/src/queue/processors/DeliverProcessorService.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { Inject, Injectable } from '@nestjs/common';
77
import * as Bull from 'bullmq';
8+
import { Not } from 'typeorm';
89
import { DI } from '@/di-symbols.js';
910
import type { InstancesRepository } from '@/models/_.js';
1011
import type Logger from '@/logger.js';
@@ -62,7 +63,7 @@ export class DeliverProcessorService {
6263
if (suspendedHosts == null) {
6364
suspendedHosts = await this.instancesRepository.find({
6465
where: {
65-
isSuspended: true,
66+
suspensionState: Not('none'),
6667
},
6768
});
6869
this.suspendedHostsCache.set(suspendedHosts);
@@ -79,6 +80,7 @@ export class DeliverProcessorService {
7980
if (i.isNotResponding) {
8081
this.federatedInstanceService.update(i.id, {
8182
isNotResponding: false,
83+
notRespondingSince: null,
8284
});
8385
}
8486

@@ -98,7 +100,15 @@ export class DeliverProcessorService {
98100
if (!i.isNotResponding) {
99101
this.federatedInstanceService.update(i.id, {
100102
isNotResponding: true,
103+
notRespondingSince: new Date(),
101104
});
105+
} else if (i.notRespondingSince) {
106+
// 1週間以上不通ならサスペンド
107+
if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
108+
this.federatedInstanceService.update(i.id, {
109+
suspensionState: 'autoSuspendedForNotResponding',
110+
});
111+
}
102112
}
103113

104114
this.apRequestChart.deliverFail();
@@ -116,7 +126,7 @@ export class DeliverProcessorService {
116126
if (job.data.isSharedInbox && res.statusCode === 410) {
117127
this.federatedInstanceService.fetch(host).then(i => {
118128
this.federatedInstanceService.update(i.id, {
119-
isSuspended: true,
129+
suspensionState: 'goneSuspended',
120130
});
121131
});
122132
throw new Bull.UnrecoverableError(`${host} is gone`);

packages/backend/src/queue/processors/InboxProcessorService.ts

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ export class InboxProcessorService {
188188
this.federatedInstanceService.update(i.id, {
189189
latestRequestReceivedAt: new Date(),
190190
isNotResponding: false,
191+
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
192+
suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
191193
});
192194

193195
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);

packages/backend/src/server/api/ApiServerService.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class ApiServerService {
137137
const instances = await this.instancesRepository.find({
138138
select: ['host'],
139139
where: {
140-
isSuspended: false,
140+
suspensionState: 'none',
141141
},
142142
});
143143

packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
4646
throw new Error('instance not found');
4747
}
4848

49+
const isSuspendedBefore = instance.suspensionState !== 'none';
50+
let suspensionState: undefined | 'manuallySuspended' | 'none';
51+
52+
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
53+
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
54+
}
55+
4956
await this.federatedInstanceService.update(instance.id, {
50-
isSuspended: ps.isSuspended,
57+
suspensionState,
5158
moderationNote: ps.moderationNote,
5259
});
5360

54-
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
61+
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
5562
if (ps.isSuspended) {
5663
this.moderationLogService.log(me, 'suspendRemoteInstance', {
5764
id: instance.id,

packages/frontend/src/pages/admin/federation.vue

+12-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
5858
</template>
5959

6060
<script lang="ts" setup>
61+
import * as Misskey from 'misskey-js';
6162
import { computed, ref } from 'vue';
6263
import XHeader from './_header_.vue';
6364
import MkInput from '@/components/MkInput.vue';
@@ -90,8 +91,17 @@ const pagination = {
9091
})),
9192
};
9293

93-
function getStatus(instance) {
94-
if (instance.isSuspended) return 'Suspended';
94+
function getStatus(instance: Misskey.entities.FederationInstance) {
95+
switch (instance.suspensionState) {
96+
case 'manuallySuspended':
97+
return 'Manually Suspended';
98+
case 'goneSuspended':
99+
return 'Automatically Suspended (Gone)';
100+
case 'autoSuspendedForNotResponding':
101+
return 'Automatically Suspended (Not Responding)';
102+
case 'none':
103+
break;
104+
}
95105
if (instance.isBlocked) return 'Blocked';
96106
if (instance.isSilenced) return 'Silenced';
97107
if (instance.isNotResponding) return 'Error';

packages/frontend/src/pages/instance-info.vue

+24-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ SPDX-License-Identifier: AGPL-3.0-only
3535
<FormSection v-if="iAmModerator">
3636
<template #label>Moderation</template>
3737
<div class="_gaps_s">
38-
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
38+
<MkKeyValue>
39+
<template #key>
40+
{{ i18n.ts._delivery.status }}
41+
</template>
42+
<template #value>
43+
{{ i18n.ts._delivery._type[suspensionState] }}
44+
</template>
45+
</MkKeyValue>
46+
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
47+
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
3948
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
4049
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
4150
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
@@ -155,7 +164,7 @@ const tab = ref('overview');
155164
const chartSrc = ref('instance-requests');
156165
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
157166
const instance = ref<Misskey.entities.FederationInstance | null>(null);
158-
const suspended = ref(false);
167+
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
159168
const isBlocked = ref(false);
160169
const isSilenced = ref(false);
161170
const faviconUrl = ref<string | null>(null);
@@ -183,7 +192,7 @@ async function fetch(): Promise<void> {
183192
instance.value = await misskeyApi('federation/show-instance', {
184193
host: props.host,
185194
});
186-
suspended.value = instance.value?.isSuspended ?? false;
195+
suspensionState.value = instance.value?.suspensionState ?? 'none';
187196
isBlocked.value = instance.value?.isBlocked ?? false;
188197
isSilenced.value = instance.value?.isSilenced ?? false;
189198
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
@@ -209,11 +218,21 @@ async function toggleSilenced(): Promise<void> {
209218
});
210219
}
211220

212-
async function toggleSuspend(): Promise<void> {
221+
async function stopDelivery(): Promise<void> {
213222
if (!instance.value) throw new Error('No instance?');
223+
suspensionState.value = 'manuallySuspended';
214224
await misskeyApi('admin/federation/update-instance', {
215225
host: instance.value.host,
216-
isSuspended: suspended.value,
226+
isSuspended: true,
227+
});
228+
}
229+
230+
async function resumeDelivery(): Promise<void> {
231+
if (!instance.value) throw new Error('No instance?');
232+
suspensionState.value = 'none';
233+
await misskeyApi('admin/federation/update-instance', {
234+
host: instance.value.host,
235+
isSuspended: false,
217236
});
218237
}
219238

packages/misskey-js/src/autogen/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4475,6 +4475,8 @@ export type components = {
44754475
followersCount: number;
44764476
isNotResponding: boolean;
44774477
isSuspended: boolean;
4478+
/** @enum {string} */
4479+
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
44784480
isBlocked: boolean;
44794481
/** @example misskey */
44804482
softwareName: string | null;

0 commit comments

Comments
 (0)