Merge branch 'develop' into feature/default-post-target-detect-from-path
This commit is contained in:
commit
250ed258d6
|
@ -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: 一部のページ内リンクが正しく動作しない問題を修正
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
||||||
/**
|
/**
|
||||||
* ユーザーを凍結する
|
* ユーザーを凍結する
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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": "ユーザーのバーナーを削除する"
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const readyRef = { value: false };
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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`);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -615,6 +615,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) {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue