From 9771f1c435d613ef5d8eecb9b32dd68527b3eefc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 29 Sep 2023 13:26:11 +0900
Subject: [PATCH] enhance: improve moderation log

---
 locales/index.d.ts                            |  3 +++
 locales/ja-JP.yml                             |  3 +++
 .../server/api/endpoints/admin/ad/create.ts   |  8 ++++++
 .../server/api/endpoints/admin/ad/delete.ts   |  8 ++++++
 .../server/api/endpoints/admin/ad/update.ts   | 11 ++++++++
 packages/backend/src/types.ts                 | 16 ++++++++++++
 .../src/pages/admin/modlog.ModLog.vue         | 26 ++++++++++++++++++-
 packages/misskey-js/etc/misskey-js.api.md     |  2 +-
 packages/misskey-js/src/consts.ts             | 16 ++++++++++++
 9 files changed, 91 insertions(+), 2 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index b971e8905b..099ebbfc7a 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2283,6 +2283,9 @@ export interface Locale {
         "unmarkSensitiveDriveFile": string;
         "resolveAbuseReport": string;
         "createInvitation": string;
+        "createAd": string;
+        "deleteAd": string;
+        "updateAd": string;
     };
 }
 declare const locales: {
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5822ba8fba..321c2c5f01 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2196,3 +2196,6 @@ _moderationLogTypes:
   unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
   resolveAbuseReport: "通報を解決"
   createInvitation: "招待コードを作成"
+  createAd: "広告を作成"
+  deleteAd: "広告を削除"
+  updateAd: "広告を更新"
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
index 2ed9410e78..e48dffecf4 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { AdsRepository } from '@/models/_.js';
 import { IdService } from '@/core/IdService.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -39,6 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		private adsRepository: AdsRepository,
 
 		private idService: IdService,
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const ad = await this.adsRepository.insert({
@@ -54,6 +56,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				place: ps.place,
 				memo: ps.memo,
 			}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
+
+			this.moderationLogService.log(me, 'createAd', {
+				adId: ad.id,
+				ad: ad,
+			});
+
 			return ad;
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
index d3c53d4f67..8097133a4c 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { AdsRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { ApiError } from '../../../error.js';
 
 export const meta = {
@@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		@Inject(DI.adsRepository)
 		private adsRepository: AdsRepository,
+
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const ad = await this.adsRepository.findOneBy({ id: ps.id });
@@ -44,6 +47,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			if (ad == null) throw new ApiError(meta.errors.noSuchAd);
 
 			await this.adsRepository.delete(ad.id);
+
+			this.moderationLogService.log(me, 'deleteAd', {
+				adId: ad.id,
+				ad: ad,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
index 5b77f67e10..d065f9ec50 100644
--- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { AdsRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { ApiError } from '../../../error.js';
 
 export const meta = {
@@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 	constructor(
 		@Inject(DI.adsRepository)
 		private adsRepository: AdsRepository,
+
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const ad = await this.adsRepository.findOneBy({ id: ps.id });
@@ -63,6 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				startsAt: new Date(ps.startsAt),
 				dayOfWeek: ps.dayOfWeek,
 			});
+
+			const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
+
+			this.moderationLogService.log(me, 'updateAd', {
+				adId: ad.id,
+				before: ad,
+				after: updatedAd,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 9e06d30aac..a9b9a55bc0 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -57,6 +57,9 @@ export const moderationLogTypes = [
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
 	'createInvitation',
+	'createAd',
+	'updateAd',
+	'deleteAd',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -202,6 +205,19 @@ export type ModerationLogPayloads = {
 	createInvitation: {
 		invitations: any[];
 	};
+	createAd: {
+		adId: string;
+		ad: any;
+	};
+	updateAd: {
+		adId: string;
+		before: any;
+		after: any;
+	};
+	deleteAd: {
+		adId: string;
+		ad: any;
+	};
 };
 
 export type Serialized<T> = {
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 8d83b32fa1..99b8544f33 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -6,7 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkFolder>
 	<template #label>
-		<b>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
+		<b
+			:class="{
+				[$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation'].includes(log.type),
+				[$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type),
+				[$style.logRed]: ['suspend', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd'].includes(log.type)
+			}"
+		>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
 		<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
 		<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
 		<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
@@ -18,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span>
 		<span v-else-if="log.type === 'addCustomEmoji'">: {{ log.info.emoji.name }}</span>
 		<span v-else-if="log.type === 'updateCustomEmoji'">: {{ log.info.before.name }}</span>
+		<span v-else-if="log.type === 'deleteCustomEmoji'">: {{ log.info.emoji.name }}</span>
 		<span v-else-if="log.type === 'markSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
 		<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
 		<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
@@ -76,6 +83,11 @@ 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"/>
 			</div>
 		</template>
+		<template v-else-if="log.type === 'updateAd'">
+			<div :class="$style.diff">
+				<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>
+		</template>
 
 		<details>
 			<summary>raw</summary>
@@ -114,4 +126,16 @@ const props = defineProps<{
 	border-radius: 6px;
 	overflow: clip;
 }
+
+.logYellow {
+	color: var(--warning);
+}
+
+.logRed {
+	color: var(--error);
+}
+
+.logGreen {
+	color: var(--success);
+}
 </style>
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 97b43ee635..d24a0624d0 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2625,7 +2625,7 @@ type ModerationLog = {
 });
 
 // @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"];
+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"];
 
 // @public (undocumented)
 export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index 14a5b5643c..271a64274f 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -75,6 +75,9 @@ export const moderationLogTypes = [
 	'unmarkSensitiveDriveFile',
 	'resolveAbuseReport',
 	'createInvitation',
+	'createAd',
+	'updateAd',
+	'deleteAd',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -220,4 +223,17 @@ export type ModerationLogPayloads = {
 	createInvitation: {
 		invitations: any[];
 	};
+	createAd: {
+		adId: string;
+		ad: any;
+	};
+	updateAd: {
+		adId: string;
+		before: any;
+		after: any;
+	};
+	deleteAd: {
+		adId: string;
+		ad: any;
+	};
 };