enhance: サーバーごとにモデレーションノートを残せるように

This commit is contained in:
syuilo 2024-02-22 20:59:52 +09:00
parent fb0eb5a31f
commit 26c8b53f70
16 changed files with 96 additions and 9 deletions

View file

@ -14,6 +14,7 @@
## 202x.x.x (unreleased) ## 202x.x.x (unreleased)
### General ### General
- Enhance: サーバーごとにモデレーションノートを残せるように
### Client ### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整

6
locales/index.d.ts vendored
View file

@ -9172,7 +9172,7 @@ export interface Locale extends ILocale {
*/ */
"updateServerSettings": string; "updateServerSettings": string;
/** /**
* *
*/ */
"updateUserNote": string; "updateUserNote": string;
/** /**
@ -9219,6 +9219,10 @@ export interface Locale extends ILocale {
* *
*/ */
"unsuspendRemoteInstance": string; "unsuspendRemoteInstance": string;
/**
*
*/
"updateRemoteInstanceNote": string;
/** /**
* *
*/ */

View file

@ -2434,7 +2434,7 @@ _moderationLogTypes:
updateCustomEmoji: "カスタム絵文字更新" updateCustomEmoji: "カスタム絵文字更新"
deleteCustomEmoji: "カスタム絵文字削除" deleteCustomEmoji: "カスタム絵文字削除"
updateServerSettings: "サーバー設定更新" updateServerSettings: "サーバー設定更新"
updateUserNote: "モデレーションノート更新" updateUserNote: "ユーザーのモデレーションノート更新"
deleteDriveFile: "ファイルを削除" deleteDriveFile: "ファイルを削除"
deleteNote: "ノートを削除" deleteNote: "ノートを削除"
createGlobalAnnouncement: "全体のお知らせを作成" createGlobalAnnouncement: "全体のお知らせを作成"
@ -2446,6 +2446,7 @@ _moderationLogTypes:
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを停止" suspendRemoteInstance: "リモートサーバーを停止"
unsuspendRemoteInstance: "リモートサーバーを再開" unsuspendRemoteInstance: "リモートサーバーを再開"
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PerInstanceModNote1708399372194 {
name = 'PerInstanceModNote1708399372194'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`);
}
}

View file

@ -8,12 +8,15 @@ import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '../UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js';
import { MiUser } from '@/models/User.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
private roleService: RoleService,
private utilityService: UtilityService, private utilityService: UtilityService,
) { ) {
@ -22,8 +25,11 @@ export class InstanceEntityService {
@bindThis @bindThis
public async pack( public async pack(
instance: MiInstance, instance: MiInstance,
me?: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
return { return {
id: instance.id, id: instance.id,
firstRetrievedAt: instance.firstRetrievedAt.toISOString(), firstRetrievedAt: instance.firstRetrievedAt.toISOString(),
@ -48,6 +54,7 @@ export class InstanceEntityService {
themeColor: instance.themeColor, themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
moderationNote: iAmModerator ? instance.moderationNote : null,
}; };
} }

View file

@ -144,4 +144,9 @@ export class MiInstance {
nullable: true, nullable: true,
}) })
public infoUpdatedAt: Date | null; public infoUpdatedAt: Date | null;
@Column('varchar', {
length: 16384, default: '',
})
public moderationNote: string;
} }

View file

@ -107,5 +107,9 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true, optional: false, nullable: true,
format: 'date-time', format: 'date-time',
}, },
moderationNote: {
type: 'string',
optional: true, nullable: true,
},
}, },
} as const; } as const;

View file

@ -24,8 +24,9 @@ export const paramDef = {
properties: { properties: {
host: { type: 'string' }, host: { type: 'string' },
isSuspended: { type: 'boolean' }, isSuspended: { type: 'boolean' },
moderationNote: { type: 'string' },
}, },
required: ['host', 'isSuspended'], required: ['host'],
} as const; } as const;
@Injectable() @Injectable()
@ -47,9 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.federatedInstanceService.update(instance.id, { await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended, isSuspended: ps.isSuspended,
moderationNote: ps.moderationNote,
}); });
if (instance.isSuspended !== ps.isSuspended) { if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended) { if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', { this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id, id: instance.id,
@ -62,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
} }
} }
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
host: instance.host,
before: instance.moderationNote,
after: ps.moderationNote,
});
}
}); });
} }
} }

View file

@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const instance = await this.instancesRepository const instance = await this.instancesRepository
.findOneBy({ host: this.utilityService.toPuny(ps.host) }); .findOneBy({ host: this.utilityService.toPuny(ps.host) });
return instance ? await this.instanceEntityService.pack(instance) : null; return instance ? await this.instanceEntityService.pack(instance, me) : null;
}); });
} }
} }

View file

@ -69,6 +69,7 @@ export const moderationLogTypes = [
'resetPassword', 'resetPassword',
'suspendRemoteInstance', 'suspendRemoteInstance',
'unsuspendRemoteInstance', 'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
@ -209,6 +210,12 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
updateRemoteInstanceNote: {
id: string;
host: string;
before: string | null;
after: string | null;
};
markSensitiveDriveFile: { markSensitiveDriveFile: {
fileId: string; fileId: string;
fileUserId: string | null; fileUserId: string | null;

View file

@ -110,6 +110,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div> </div>
</template> </template>
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<details> <details>
<summary>raw</summary> <summary>raw</summary>

View file

@ -39,6 +39,9 @@ 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>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
</MkTextarea>
</div> </div>
</FormSection> </FormSection>
@ -119,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue'; import MkObjectView from '@/components/MkObjectView.vue';
@ -141,6 +144,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js'; import { dateString } from '@/filters/date.js';
import MkTextarea from '@/components/MkTextarea.vue';
const props = defineProps<{ const props = defineProps<{
host: string; host: string;
@ -155,6 +159,7 @@ const suspended = ref(false);
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);
const moderationNote = ref('');
const usersPagination = { const usersPagination = {
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
@ -167,6 +172,10 @@ const usersPagination = {
offsetMode: true, offsetMode: true,
}; };
watch(moderationNote, async () => {
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
});
async function fetch(): Promise<void> { async function fetch(): Promise<void> {
if (iAmAdmin) { if (iAmAdmin) {
meta.value = await misskeyApi('admin/meta'); meta.value = await misskeyApi('admin/meta');
@ -178,6 +187,7 @@ async function fetch(): Promise<void> {
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');
moderationNote.value = instance.value?.moderationNote;
} }
async function toggleBlock(): Promise<void> { async function toggleBlock(): Promise<void> {

View file

@ -2316,6 +2316,9 @@ type ModerationLog = {
} | { } | {
type: 'unsuspendRemoteInstance'; type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance']; info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | { } | {
type: 'markSensitiveDriveFile'; type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile']; info: ModerationLogPayloads['markSensitiveDriveFile'];
@ -2355,7 +2358,7 @@ type ModerationLog = {
}); });
// @public (undocumented) // @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
// @public (undocumented) // @public (undocumented)
type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json'];

View file

@ -4480,6 +4480,7 @@ export type components = {
infoUpdatedAt: string | null; infoUpdatedAt: string | null;
/** Format: date-time */ /** Format: date-time */
latestRequestReceivedAt: string | null; latestRequestReceivedAt: string | null;
moderationNote?: string | null;
}; };
GalleryPost: { GalleryPost: {
/** /**
@ -7213,7 +7214,8 @@ export type operations = {
content: { content: {
'application/json': { 'application/json': {
host: string; host: string;
isSuspended: boolean; isSuspended?: boolean;
moderationNote?: string;
}; };
}; };
}; };

View file

@ -121,6 +121,7 @@ export const moderationLogTypes = [
'resetPassword', 'resetPassword',
'suspendRemoteInstance', 'suspendRemoteInstance',
'unsuspendRemoteInstance', 'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
@ -261,6 +262,12 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
updateRemoteInstanceNote: {
id: string;
host: string;
before: string | null;
after: string | null;
};
markSensitiveDriveFile: { markSensitiveDriveFile: {
fileId: string; fileId: string;
fileUserId: string | null; fileUserId: string | null;

View file

@ -95,6 +95,9 @@ export type ModerationLog = {
} | { } | {
type: 'unsuspendRemoteInstance'; type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance']; info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | { } | {
type: 'markSensitiveDriveFile'; type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile']; info: ModerationLogPayloads['markSensitiveDriveFile'];