Merge remote-tracking branch 'upstream/develop' into forward_hashtag_search

This commit is contained in:
Sayamame-beans 2024-05-25 10:45:44 +09:00
commit a1513dd0a7
29 changed files with 297 additions and 32 deletions

View file

@ -3,6 +3,7 @@
### Note ### Note
- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。 - コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。 - 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。
- 管理者向け権限 `read:admin:show-users``read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。
### General ### General
- Enhance: URLプレビューの有効化・無効化を設定できるように #13569 - Enhance: URLプレビューの有効化・無効化を設定できるように #13569
@ -15,6 +16,9 @@
- サスペンド済みユーザーか - サスペンド済みユーザーか
- 鍵アカウントユーザーか - 鍵アカウントユーザーか
- 「アカウントを見つけやすくする」が有効なユーザーか - 「アカウントを見つけやすくする」が有効なユーザーか
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
- Enhance: 配信停止の理由を表示するように
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 - Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正 - Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正 - Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
@ -42,6 +46,7 @@
- Enhance: `Ui:C:postForm` および `Ui:C:postFormButton``localOnly``visibility` を設定できるように - Enhance: `Ui:C:postForm` および `Ui:C:postFormButton``localOnly``visibility` を設定できるように
- Enhance: AiScriptを0.18.0にバージョンアップ - Enhance: AiScriptを0.18.0にバージョンアップ
- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように - Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように
- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように
- Enhance: ノート検索で `#` から始まる文字列を入力すると、そのハッシュタグのノート一覧ページが表示されるように - Enhance: ノート検索で `#` から始まる文字列を入力すると、そのハッシュタグのノート一覧ページが表示されるように
- Enhance: ユーザー検索で `#` から始まる文字列を入力すると、そのハッシュタグのユーザー一覧ページが表示されるように - Enhance: ユーザー検索で `#` から始まる文字列を入力すると、そのハッシュタグのユーザー一覧ページが表示されるように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正 - Fix: 一部のページ内リンクが正しく動作しない問題を修正

View file

@ -4,4 +4,4 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}') PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}')
curl -s -S -o /dev/null "http://localhost:${PORT}" curl -Sfso/dev/null "http://localhost:${PORT}/healthz"

View file

@ -654,7 +654,7 @@ smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest."
testEmail: "Emailversand testen" testEmail: "Emailversand testen"
wordMute: "Wortstummschaltung" wordMute: "Wortstummschaltung"
regexpError: "Fehler in einem regulären Ausdruck" regexpError: "Fehler in einem regulären Ausdruck"
regexpErrorDescription: "Im regulären Ausdruck deiner {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:" regexpErrorDescription: "Im regulären Ausdruck deiner in Zeile {line} von {tab}en Wortstummschaltungen ist ein Fehler aufgetreten:"
instanceMute: "Instanzstummschaltungen" instanceMute: "Instanzstummschaltungen"
userSaysSomething: "{name} hat etwas gesagt" userSaysSomething: "{name} hat etwas gesagt"
makeActive: "Aktivieren" makeActive: "Aktivieren"

42
locales/index.d.ts vendored
View file

@ -917,7 +917,7 @@ export interface Locale extends ILocale {
*/ */
"silencedInstances": string; "silencedInstances": string;
/** /**
* *
*/ */
"silencedInstancesDescription": string; "silencedInstancesDescription": string;
/** /**
@ -1900,6 +1900,10 @@ export interface Locale extends ILocale {
* *
*/ */
"quoteQuestion": string; "quoteQuestion": string;
/**
*
*/
"attachAsFileQuestion": string;
/** /**
* *
*/ */
@ -4968,6 +4972,38 @@ export interface Locale extends ILocale {
* *
*/ */
"inquiry": string; "inquiry": string;
"_delivery": {
/**
*
*/
"status": string;
/**
*
*/
"stop": string;
/**
*
*/
"resume": string;
"_type": {
/**
*
*/
"none": string;
/**
*
*/
"manuallySuspended": string;
/**
*
*/
"goneSuspended": string;
/**
*
*/
"autoSuspendedForNotResponding": string;
};
};
"_bubbleGame": { "_bubbleGame": {
/** /**
* *
@ -7899,10 +7935,6 @@ export interface Locale extends ILocale {
* *
*/ */
"read:admin:show-user": string; "read:admin:show-user": string;
/**
*
*/
"read:admin:show-users": string;
/** /**
* *
*/ */

View file

@ -471,6 +471,7 @@ retype: "再入力"
noteOf: "{user}のノート" noteOf: "{user}のノート"
quoteAttached: "引用付き" quoteAttached: "引用付き"
quoteQuestion: "引用として添付しますか?" quoteQuestion: "引用として添付しますか?"
attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?"
noMessagesYet: "まだチャットはありません" noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります" newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
@ -1239,6 +1240,16 @@ noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する" alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ" inquiry: "お問い合わせ"
_delivery:
status: "配信状態"
stop: "配信停止"
resume: "配信再開"
_type:
none: "配信中"
manuallySuspended: "手動停止中"
goneSuspended: "サーバー削除のため停止中"
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"
hold: "ホールド" hold: "ホールド"
@ -2074,7 +2085,6 @@ _permissions:
"read:admin:server-info": "サーバーの情報を見る" "read:admin:server-info": "サーバーの情報を見る"
"read:admin:show-moderation-log": "モデレーションログを見る" "read:admin:show-moderation-log": "モデレーションログを見る"
"read:admin:show-user": "ユーザーのプライベートな情報を見る" "read:admin:show-user": "ユーザーのプライベートな情報を見る"
"read:admin:show-users": "ユーザーのプライベートな情報を見る"
"write:admin:suspend-user": "ユーザーを凍結する" "write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する" "write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する" "write:admin:unset-user-banner": "ユーザーのバーナーを削除する"

View file

@ -933,7 +933,7 @@ check: "Zweryfikuj"
driveCapOverrideLabel: "Zmień limit pojemności dysku użytkownika" driveCapOverrideLabel: "Zmień limit pojemności dysku użytkownika"
requireAdminForView: "Aby to zobaczyć, musisz być administratorem" requireAdminForView: "Aby to zobaczyć, musisz być administratorem"
isSystemAccount: "To jest konto stworzone i zarządzane przez system" isSystemAccount: "To jest konto stworzone i zarządzane przez system"
typeToConfirm: "Wielki chuj " typeToConfirm: "Wprowadź {x}, aby potwierdzić"
deleteAccount: "Usuń konto" deleteAccount: "Usuń konto"
document: "Dokumentacja" document: "Dokumentacja"
numberOfPageCache: "Ilość stron w cache" numberOfPageCache: "Ilość stron w cache"

View file

@ -108,11 +108,14 @@ enterEmoji: "输入表情符号"
renote: "转发" renote: "转发"
unrenote: "取消转发" unrenote: "取消转发"
renoted: "已转发。" renoted: "已转发。"
renotedToX: "转帖给 {name}"
cantRenote: "该帖无法转发。" cantRenote: "该帖无法转发。"
cantReRenote: "转发无法被再次转发。" cantReRenote: "转发无法被再次转发。"
quote: "引用" quote: "引用"
inChannelRenote: "在频道内转发" inChannelRenote: "在频道内转发"
inChannelQuote: "在频道内引用" inChannelQuote: "在频道内引用"
renoteToChannel: "转帖至频道"
renoteToOtherChannel: "转帖至其它频道"
pinnedNote: "已置顶的帖子" pinnedNote: "已置顶的帖子"
pinned: "置顶" pinned: "置顶"
you: "您" you: "您"

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class NotRespondingSince1716345015347 {
name = 'NotRespondingSince1716345015347'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "notRespondingSince" TIMESTAMP WITH TIME ZONE`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "notRespondingSince"`);
}
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SuspensionStateInsteadOfIsSspended1716345771510 {
name = 'SuspensionStateInsteadOfIsSspended1716345771510'
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."instance_suspensionstate_enum" AS ENUM('none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding')`);
await queryRunner.query(`DROP INDEX "public"."IDX_34500da2e38ac393f7bb6b299c"`);
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "isSuspended" TO "suspensionState"`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE "public"."instance_suspensionstate_enum" USING (
CASE "suspensionState"
WHEN TRUE THEN 'manuallySuspended'::instance_suspensionstate_enum
ELSE 'none'::instance_suspensionstate_enum
END
)`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT 'none'`);
await queryRunner.query(`CREATE INDEX "IDX_3ede46f507c87ad698051d56a8" ON "instance" ("suspensionState") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_3ede46f507c87ad698051d56a8"`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" TYPE boolean USING (
CASE "suspensionState"
WHEN 'none'::instance_suspensionstate_enum THEN FALSE
ELSE TRUE
END
)`);
await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "suspensionState" SET DEFAULT false`);
await queryRunner.query(`ALTER TABLE "instance" RENAME COLUMN "suspensionState" TO "isSuspended"`);
await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `);
await queryRunner.query(`DROP TYPE "public"."instance_suspensionstate_enum"`);
}
}

View file

@ -15,6 +15,7 @@ import Logger from '@/logger.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
import { masterMain } from './master.js'; import { masterMain } from './master.js';
import { workerMain } from './worker.js'; import { workerMain } from './worker.js';
import { readyRef } from './ready.js';
import 'reflect-metadata'; import 'reflect-metadata';
@ -79,6 +80,8 @@ if (cluster.isWorker || envOption.disableClustering) {
await workerMain(); await workerMain();
} }
readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため // ユニットテスト時にMisskeyが子プロセスで起動された時のため
// それ以外のときは process.send は使えないので弾く // それ以外のときは process.send は使えないので弾く
if (process.send) { if (process.send) {

View file

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const readyRef = { value: false };

View file

@ -39,7 +39,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount, followingCount: instance.followingCount,
followersCount: instance.followersCount, followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended, isSuspended: instance.suspensionState !== 'none',
suspensionState: instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
softwareName: instance.softwareName, softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion, softwareVersion: instance.softwareVersion,

View file

@ -81,13 +81,22 @@ export class MiInstance {
public isNotResponding: boolean; public isNotResponding: boolean;
/** /**
* *
*/
@Column('timestamp with time zone', {
nullable: true,
})
public notRespondingSince: Date | null;
/**
*
*/ */
@Index() @Index()
@Column('boolean', { @Column('enum', {
default: false, default: 'none',
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
}) })
public isSuspended: boolean; public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
@Column('varchar', { @Column('varchar', {
length: 64, nullable: true, length: 64, nullable: true,

View file

@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
suspensionState: {
type: 'string',
nullable: false, optional: false,
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
},
isBlocked: { isBlocked: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/_.js'; import type { InstancesRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
@ -62,7 +63,7 @@ export class DeliverProcessorService {
if (suspendedHosts == null) { if (suspendedHosts == null) {
suspendedHosts = await this.instancesRepository.find({ suspendedHosts = await this.instancesRepository.find({
where: { where: {
isSuspended: true, suspensionState: Not('none'),
}, },
}); });
this.suspendedHostsCache.set(suspendedHosts); this.suspendedHostsCache.set(suspendedHosts);
@ -79,6 +80,7 @@ export class DeliverProcessorService {
if (i.isNotResponding) { if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
isNotResponding: false, isNotResponding: false,
notRespondingSince: null,
}); });
} }
@ -98,7 +100,15 @@ export class DeliverProcessorService {
if (!i.isNotResponding) { if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
isNotResponding: true, isNotResponding: true,
notRespondingSince: new Date(),
}); });
} else if (i.notRespondingSince) {
// 1週間以上不通ならサスペンド
if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
this.federatedInstanceService.update(i.id, {
suspensionState: 'autoSuspendedForNotResponding',
});
}
} }
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();
@ -116,7 +126,7 @@ export class DeliverProcessorService {
if (job.data.isSharedInbox && res.statusCode === 410) { if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => { this.federatedInstanceService.fetch(host).then(i => {
this.federatedInstanceService.update(i.id, { this.federatedInstanceService.update(i.id, {
isSuspended: true, suspensionState: 'goneSuspended',
}); });
}); });
throw new Bull.UnrecoverableError(`${host} is gone`); throw new Bull.UnrecoverableError(`${host} is gone`);

View file

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

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import { readyRef } from '@/boot/ready.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
import type { MeiliSearch } from 'meilisearch';
@Injectable()
export class HealthServerService {
constructor(
@Inject(DI.redis)
private redis: Redis.Redis,
@Inject(DI.redisForPub)
private redisForPub: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.db)
private db: DataSource,
@Inject(DI.meilisearch)
private meilisearch: MeiliSearch | null,
) {}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/', async (request, reply) => {
reply.code(await Promise.all([
new Promise<void>((resolve, reject) => readyRef.value ? resolve() : reject()),
this.redis.ping(),
this.redisForPub.ping(),
this.redisForSub.ping(),
this.redisForTimelines.ping(),
this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503));
reply.header('Cache-Control', 'no-store');
});
done();
}
}

View file

@ -8,6 +8,7 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js'; import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js'; import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js';
@ -55,6 +56,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ClientServerService, ClientServerService,
ClientLoggerService, ClientLoggerService,
FeedService, FeedService,
HealthServerService,
UrlPreviewService, UrlPreviewService,
ActivityPubServerService, ActivityPubServerService,
FileServerService, FileServerService,

View file

@ -28,6 +28,7 @@ import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js'; import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@ -61,6 +62,7 @@ export class ServerService implements OnApplicationShutdown {
private wellKnownServerService: WellKnownServerService, private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService, private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService, private fileServerService: FileServerService,
private healthServerService: HealthServerService,
private clientServerService: ClientServerService, private clientServerService: ClientServerService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private loggerService: LoggerService, private loggerService: LoggerService,
@ -108,6 +110,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.wellKnownServerService.createServer); fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' }); fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;

View file

@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({ const instances = await this.instancesRepository.find({
select: ['host'], select: ['host'],
where: { where: {
isSuspended: false, suspensionState: 'none',
}, },
}); });

View file

@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found'); throw new Error('instance not found');
} }
const isSuspendedBefore = instance.suspensionState !== 'none';
let suspensionState: undefined | 'manuallySuspended' | 'none';
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
}
await this.federatedInstanceService.update(instance.id, { await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended, suspensionState,
moderationNote: ps.moderationNote, moderationNote: ps.moderationNote,
}); });
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) { if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) { if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', { this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id, id: instance.id,

View file

@ -16,7 +16,7 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
kind: 'read:admin:show-users', kind: 'read:admin:show-user',
res: { res: {
type: 'array', type: 'array',

View file

@ -612,6 +612,23 @@ async function onPaste(ev: ClipboardEvent) {
quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null; quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
}); });
} }
if (paste.length > 1000) {
ev.preventDefault();
os.confirm({
type: 'info',
text: i18n.ts.attachAsFileQuestion,
}).then(({ canceled }) => {
if (canceled) {
insertTextAtCursor(textareaEl.value, paste);
return;
}
const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0");
const file = new File([paste], `${fileName}.txt`, { type: "text/plain" });
upload(file, `${fileName}.txt`);
});
}
} }
function onDragover(ev) { function onDragover(ev) {

View file

@ -58,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -90,8 +91,17 @@ const pagination = {
})), })),
}; };
function getStatus(instance) { function getStatus(instance: Misskey.entities.FederationInstance) {
if (instance.isSuspended) return 'Suspended'; switch (instance.suspensionState) {
case 'manuallySuspended':
return 'Manually Suspended';
case 'goneSuspended':
return 'Automatically Suspended (Gone)';
case 'autoSuspendedForNotResponding':
return 'Automatically Suspended (Not Responding)';
case 'none':
break;
}
if (instance.isBlocked) return 'Blocked'; if (instance.isBlocked) return 'Blocked';
if (instance.isSilenced) return 'Silenced'; if (instance.isSilenced) return 'Silenced';
if (instance.isNotResponding) return 'Error'; if (instance.isNotResponding) return 'Error';

View file

@ -35,7 +35,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="iAmModerator"> <FormSection v-if="iAmModerator">
<template #label>Moderation</template> <template #label>Moderation</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> <MkKeyValue>
<template #key>
{{ i18n.ts._delivery.status }}
</template>
<template #value>
{{ i18n.ts._delivery._type[suspensionState] }}
</template>
</MkKeyValue>
<MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton>
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
<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>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
@ -155,7 +164,7 @@ const tab = ref('overview');
const chartSrc = ref('instance-requests'); const chartSrc = ref('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null); const instance = ref<Misskey.entities.FederationInstance | null>(null);
const suspended = ref(false); const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
const isBlocked = ref(false); const isBlocked = ref(false);
const isSilenced = ref(false); const isSilenced = ref(false);
const faviconUrl = ref<string | null>(null); const faviconUrl = ref<string | null>(null);
@ -183,7 +192,7 @@ async function fetch(): Promise<void> {
instance.value = await misskeyApi('federation/show-instance', { instance.value = await misskeyApi('federation/show-instance', {
host: props.host, host: props.host,
}); });
suspended.value = instance.value?.isSuspended ?? false; suspensionState.value = instance.value?.suspensionState ?? 'none';
isBlocked.value = instance.value?.isBlocked ?? false; isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? false; isSilenced.value = instance.value?.isSilenced ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
@ -209,11 +218,21 @@ async function toggleSilenced(): Promise<void> {
}); });
} }
async function toggleSuspend(): Promise<void> { async function stopDelivery(): Promise<void> {
if (!instance.value) throw new Error('No instance?'); if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'manuallySuspended';
await misskeyApi('admin/federation/update-instance', { await misskeyApi('admin/federation/update-instance', {
host: instance.value.host, host: instance.value.host,
isSuspended: suspended.value, isSuspended: true,
});
}
async function resumeDelivery(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
suspensionState.value = 'none';
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
isSuspended: false,
}); });
} }

View file

@ -2626,7 +2626,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
function parse(acct: string): Acct; function parse(acct: string): Acct;
// @public (undocumented) // @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented) // @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json']; type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -678,7 +678,7 @@ declare module '../api.js' {
/** /**
* No description provided. * No description provided.
* *
* **Credential required**: *Yes* / **Permission**: *read:admin:show-users* * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/ */
request<E extends 'admin/show-users', P extends Endpoints[E]['req']>( request<E extends 'admin/show-users', P extends Endpoints[E]['req']>(
endpoint: E, endpoint: E,

View file

@ -567,7 +567,7 @@ export type paths = {
* admin/show-users * admin/show-users
* @description No description provided. * @description No description provided.
* *
* **Credential required**: *Yes* / **Permission**: *read:admin:show-users* * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/ */
post: operations['admin___show-users']; post: operations['admin___show-users'];
}; };
@ -4475,6 +4475,8 @@ export type components = {
followersCount: number; followersCount: number;
isNotResponding: boolean; isNotResponding: boolean;
isSuspended: boolean; isSuspended: boolean;
/** @enum {string} */
suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
isBlocked: boolean; isBlocked: boolean;
/** @example misskey */ /** @example misskey */
softwareName: string | null; softwareName: string | null;
@ -8645,7 +8647,7 @@ export type operations = {
* admin/show-users * admin/show-users
* @description No description provided. * @description No description provided.
* *
* **Credential required**: *Yes* / **Permission**: *read:admin:show-users* * **Credential required**: *Yes* / **Permission**: *read:admin:show-user*
*/ */
'admin___show-users': { 'admin___show-users': {
requestBody: { requestBody: {

View file

@ -58,7 +58,6 @@ export const permissions = [
'read:admin:server-info', 'read:admin:server-info',
'read:admin:show-moderation-log', 'read:admin:show-moderation-log',
'read:admin:show-user', 'read:admin:show-user',
'read:admin:show-users',
'write:admin:suspend-user', 'write:admin:suspend-user',
'write:admin:unset-user-avatar', 'write:admin:unset-user-avatar',
'write:admin:unset-user-banner', 'write:admin:unset-user-banner',