From 83ea0395f6a884b2de43b3e5bb93d1ceb107df64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
 <46447427+samunohito@users.noreply.github.com>
Date: Fri, 17 Nov 2023 22:26:54 +0900
Subject: [PATCH 01/10] =?UTF-8?q?DeepL=20Translation=E3=81=AEPro=20account?=
 =?UTF-8?q?=E3=83=88=E3=82=B0=E3=83=AB=E3=82=B9=E3=82=A4=E3=83=83=E3=83=81?=
 =?UTF-8?q?=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84?=
 =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3=20(#12355)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
---
 packages/frontend/src/pages/admin/external-services.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index e88860166c..e614bfeb1b 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -38,6 +38,7 @@ import { } from 'vue';
 import XHeader from './_header_.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';

From 0a73973a7c6e6e95a5206bfc5388ff7f7a9ba8ed Mon Sep 17 00:00:00 2001
From: Nafu Satsuki <satsuki@nafusoft.dev>
Date: Sat, 18 Nov 2023 20:39:48 +0900
Subject: [PATCH 02/10] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?=
 =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E3=81=AE=E8=AA=8D=E8=A8=BC=E3=81=AB?=
 =?UTF-8?q?verifymail.io=E3=82=92=E4=BD=BF=E3=81=88=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../1700303245007-supportVerifyMailApi.js     | 18 ++++
 packages/backend/src/core/EmailService.ts     | 92 +++++++++++++++++--
 packages/backend/src/models/Meta.ts           | 11 +++
 .../server/api/endpoints/admin/update-meta.ts | 14 +++
 .../frontend/src/pages/admin/security.vue     | 13 +++
 5 files changed, 140 insertions(+), 8 deletions(-)
 create mode 100644 packages/backend/migration/1700303245007-supportVerifyMailApi.js

diff --git a/packages/backend/migration/1700303245007-supportVerifyMailApi.js b/packages/backend/migration/1700303245007-supportVerifyMailApi.js
new file mode 100644
index 0000000000..3ac59ec37a
--- /dev/null
+++ b/packages/backend/migration/1700303245007-supportVerifyMailApi.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class SupportVerifyMailApi1700303245007 {
+    name = 'SupportVerifyMailApi1700303245007'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`);
+        await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`);
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`);
+    }
+}
diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index c9da3f77c0..8f28197ebc 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -13,6 +13,9 @@ import type Logger from '@/logger.js';
 import type { UserProfilesRepository } from '@/models/_.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
+import {URLSearchParams} from "node:url";
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import {SubOutputFormat} from "deep-email-validator/dist/output/output.js";
 
 @Injectable()
 export class EmailService {
@@ -27,6 +30,7 @@ export class EmailService {
 
 		private metaService: MetaService,
 		private loggerService: LoggerService,
+		private httpRequestService: HttpRequestService,
 	) {
 		this.logger = this.loggerService.getLogger('email');
 	}
@@ -160,14 +164,25 @@ export class EmailService {
 			email: emailAddress,
 		});
 
-		const validated = meta.enableActiveEmailValidation ? await validateEmail({
-			email: emailAddress,
-			validateRegex: true,
-			validateMx: true,
-			validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
-			validateDisposable: true, // 捨てアドかどうかチェック
-			validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
-		}) : { valid: true, reason: null };
+		const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
+		let validated;
+
+		if (meta.enableActiveEmailValidation) {
+			if (verifymailApi) {
+				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
+			} else {
+				validated = meta.enableActiveEmailValidation ? await validateEmail({
+					email: emailAddress,
+					validateRegex: true,
+					validateMx: true,
+					validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
+					validateDisposable: true, // 捨てアドかどうかチェック
+					validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
+				}) : { valid: true, reason: null };
+			}
+		} else {
+			validated = { valid: true, reason: null };
+		}
 
 		const available = exist === 0 && validated.valid;
 
@@ -182,4 +197,65 @@ export class EmailService {
 			null,
 		};
 	}
+
+	private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{
+		valid: boolean;
+		reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null;
+	}> {
+		const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey;
+		const res = await this.httpRequestService.send(endpoint, {
+			method: 'GET',
+			headers: {
+				'Content-Type': 'application/x-www-form-urlencoded',
+				Accept: 'application/json, */*',
+			},
+		});
+
+		const json = (await res.json()) as {
+			block: boolean;
+			catch_all: boolean;
+			deliverable_email: boolean;
+			disposable: boolean;
+			domain: string;
+			email_address: string;
+			email_provider: string;
+			mx: boolean;
+			mx_fallback: boolean;
+			mx_host: string[];
+			mx_ip: string[];
+			mx_priority: { [key: string]: number };
+			privacy: boolean;
+			related_domains: string[];
+		};
+
+		if (json.email_address === undefined) {
+			return {
+				valid: false,
+				reason: 'format',
+			};
+		}
+		if (json.deliverable_email !== undefined && !json.deliverable_email) {
+			return {
+				valid: false,
+				reason: 'smtp',
+			};
+		}
+		if (json.disposable) {
+			return {
+				valid: false,
+				reason: 'disposable',
+			};
+		}
+		if (json.mx !== undefined && !json.mx) {
+			return {
+				valid: false,
+				reason: 'mx',
+			};
+		}
+
+		return {
+			valid: true,
+			reason: null,
+		};
+	}
 }
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 14a72add1d..83e8962f5d 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -446,6 +446,17 @@ export class MiMeta {
 	})
 	public enableActiveEmailValidation: boolean;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableVerifymailApi: boolean;
+
+	@Column('varchar', {
+		length: 1024,
+		nullable: true,
+	})
+	public verifymailAuthKey: string | null;
+
 	@Column('boolean', {
 		default: true,
 	})
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index da3e5dd9ac..d6f9b2cd94 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -113,6 +113,8 @@ export const paramDef = {
 		objectStorageS3ForcePathStyle: { type: 'boolean' },
 		enableIpLogging: { type: 'boolean' },
 		enableActiveEmailValidation: { type: 'boolean' },
+		enableVerifymailApi: { type: 'boolean' },
+		verifymailAuthKey: { type: 'string', nullable: true },
 		enableChartsForRemoteUser: { type: 'boolean' },
 		enableChartsForFederatedInstances: { type: 'boolean' },
 		enableServerMachineStats: { type: 'boolean' },
@@ -454,6 +456,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
 			}
 
+			if (ps.enableVerifymailApi !== undefined) {
+				set.enableVerifymailApi = ps.enableVerifymailApi;
+			}
+
+			if (ps.verifymailAuthKey !== undefined) {
+				if (ps.verifymailAuthKey === '') {
+					set.verifymailAuthKey = null;
+				} else {
+					set.verifymailAuthKey = ps.verifymailAuthKey;
+				}
+			}
+
 			if (ps.enableChartsForRemoteUser !== undefined) {
 				set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
 			}
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index a2594ee6c5..f7f76d910a 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -73,6 +73,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
 							<template #label>Enable</template>
 						</MkSwitch>
+						<MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
+							<template #label>Use Verifymail API</template>
+						</MkSwitch>
+						<MkInput v-model="verifymailAuthKey" @update:modelValue="save">
+							<template #prefix><i class="ti ti-key"></i></template>
+							<template #label>Verifymail API Auth Key</template>
+						</MkInput>
 					</div>
 				</MkFolder>
 
@@ -132,6 +139,8 @@ let setSensitiveFlagAutomatically: boolean = $ref(false);
 let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
 let enableIpLogging: boolean = $ref(false);
 let enableActiveEmailValidation: boolean = $ref(false);
+let enableVerifymailApi: boolean = $ref(false);
+let verifymailAuthKey: string | null = $ref(null);
 
 async function init() {
 	const meta = await os.api('admin/meta');
@@ -150,6 +159,8 @@ async function init() {
 	enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
 	enableIpLogging = meta.enableIpLogging;
 	enableActiveEmailValidation = meta.enableActiveEmailValidation;
+	enableVerifymailApi = meta.enableVerifymailApi;
+	verifymailAuthKey = meta.verifymailAuthKey;
 }
 
 function save() {
@@ -167,6 +178,8 @@ function save() {
 		enableSensitiveMediaDetectionForVideos,
 		enableIpLogging,
 		enableActiveEmailValidation,
+		enableVerifymailApi,
+		verifymailAuthKey,
 	}).then(() => {
 		fetchInstance();
 	});

From af668b15c447d1c175cd3669fe0025a3aff61d73 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 18 Nov 2023 21:03:01 +0900
Subject: [PATCH 03/10] Update CHANGELOG.md

---
 CHANGELOG.md | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92e02508fe..4b08d12093 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,17 @@
 
 -->
 
+## 2023.x.x (unreleased)
+
+### General
+- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
+
+### Client
+-
+
+### Server
+-
+
 ## 2023.11.1
 
 ### General

From 30dc6e691d09073a904a40beb94370b6ad3c5c5c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 18 Nov 2023 21:04:00 +0900
Subject: [PATCH 04/10] lint fix

---
 packages/backend/src/core/EmailService.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts
index 8f28197ebc..f31cec2b3a 100644
--- a/packages/backend/src/core/EmailService.ts
+++ b/packages/backend/src/core/EmailService.ts
@@ -3,9 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { URLSearchParams } from 'node:url';
 import * as nodemailer from 'nodemailer';
 import { Inject, Injectable } from '@nestjs/common';
 import { validate as validateEmail } from 'deep-email-validator';
+import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js';
 import { MetaService } from '@/core/MetaService.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
@@ -13,9 +15,7 @@ import type Logger from '@/logger.js';
 import type { UserProfilesRepository } from '@/models/_.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
-import {URLSearchParams} from "node:url";
 import { HttpRequestService } from '@/core/HttpRequestService.js';
-import {SubOutputFormat} from "deep-email-validator/dist/output/output.js";
 
 @Injectable()
 export class EmailService {
@@ -167,7 +167,7 @@ export class EmailService {
 		const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
 		let validated;
 
-		if (meta.enableActiveEmailValidation) {
+		if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) {
 			if (verifymailApi) {
 				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
 			} else {

From 2b6f789a5bc741d9092d29ea17d03be794ef5d51 Mon Sep 17 00:00:00 2001
From: Nafu Satsuki <satsuki@nafusoft.dev>
Date: Sat, 18 Nov 2023 05:20:11 +0900
Subject: [PATCH 05/10] =?UTF-8?q?feat(moderation):=20=E3=83=A2=E3=83=87?=
 =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=BF=E3=83=BC=E3=81=8C=E3=83=A6=E3=83=BC?=
 =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3?=
 =?UTF-8?q?=E3=82=82=E3=81=97=E3=81=8F=E3=81=AF=E3=83=90=E3=83=8A=E3=83=BC?=
 =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=82=92=E6=9C=AA=E8=A8=AD=E5=AE=9A=E7=8A=B6?=
 =?UTF-8?q?=E6=85=8B=E3=81=AB=E3=81=A7=E3=81=8D=E3=82=8B=E6=A9=9F=E8=83=BD?=
 =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(MisskeyIO#222)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
---
 locales/en-US.yml                             |  4 ++
 locales/index.d.ts                            |  4 ++
 locales/ja-JP.yml                             |  4 ++
 .../backend/src/server/api/EndpointsModule.ts |  8 ++++
 packages/backend/src/server/api/endpoints.ts  |  4 ++
 .../api/endpoints/admin/delete-user-avatar.ts | 48 +++++++++++++++++++
 .../api/endpoints/admin/delete-user-banner.ts | 48 +++++++++++++++++++
 packages/frontend/src/pages/admin-user.vue    | 42 ++++++++++++++++
 packages/misskey-js/etc/misskey-js.api.md     | 12 +++++
 packages/misskey-js/src/api.types.ts          |  2 +
 10 files changed, 176 insertions(+)
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 09fd726c9f..2aba028e45 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -564,6 +564,10 @@ output: "Output"
 script: "Script"
 disablePagesScript: "Disable AiScript on Pages"
 updateRemoteUser: "Update remote user information"
+deleteUserAvatar: "Delete user icon"
+deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
+deleteUserBanner: "Delete user banner"
+deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
 deleteAllFiles: "Delete all files"
 deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
 removeAllFollowing: "Unfollow all followed users"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 6baed91c42..6fd6d3641a 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -567,6 +567,10 @@ export interface Locale {
     "script": string;
     "disablePagesScript": string;
     "updateRemoteUser": string;
+    "deleteUserAvatar": string;
+    "deleteUserAvatarConfirm": string;
+    "deleteUserBanner": string;
+    "deleteUserBannerConfirm": string;
     "deleteAllFiles": string;
     "deleteAllFilesConfirm": string;
     "removeAllFollowing": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index e59a550df5..9685e9c5a5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -564,6 +564,10 @@ output: "出力"
 script: "スクリプト"
 disablePagesScript: "Pagesのスクリプトを無効にする"
 updateRemoteUser: "リモートユーザー情報の更新"
+deleteUserAvatar: "アイコンを削除"
+deleteUserAvatarConfirm: "アイコンを削除しますか?"
+deleteUserBanner: "バナーを削除"
+deleteUserBannerConfirm: "バナーを削除しますか?"
 deleteAllFiles: "すべてのファイルを削除"
 deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
 removeAllFollowing: "フォローを全解除"
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 6d813ae04e..3797b46d04 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
 import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
 import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
+import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
+import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -383,6 +385,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de
 const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
 const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
 const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
+const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default };
+const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default };
 const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
 const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
 const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
@@ -746,6 +750,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_avatarDecorations_list,
 		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
+		$admin_deleteUserAvatar,
+		$admin_deleteUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
@@ -1103,6 +1109,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_avatarDecorations_list,
 		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
+		$admin_deleteUserAvatar,
+		$admin_deleteUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index ef4bd3e2b5..4162ace337 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
 import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
 import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
+import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
+import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -381,6 +383,8 @@ const eps = [
 	['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
 	['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
 	['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
+	['admin/delete-user-avatar', ep___admin_deleteUserAvatar],
+	['admin/delete-user-banner', ep___admin_deleteUserBanner],
 	['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
 	['admin/drive/cleanup', ep___admin_drive_cleanup],
 	['admin/drive/files', ep___admin_drive_files],
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
new file mode 100644
index 0000000000..d3c78d7fb6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const user = await this.usersRepository.findOneBy({ id: ps.userId });
+
+			if (user == null) {
+				throw new Error('user not found');
+			}
+
+			await this.usersRepository.update(user.id, {
+				avatar: null,
+				avatarId: null,
+				avatarUrl: null,
+				avatarBlurhash: null,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
new file mode 100644
index 0000000000..e076cdcfc1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const user = await this.usersRepository.findOneBy({ id: ps.userId });
+
+			if (user == null) {
+				throw new Error('user not found');
+			}
+
+			await this.usersRepository.update(user.id, {
+				banner: null,
+				bannerId: null,
+				bannerUrl: null,
+				bannerBlurhash: null,
+			});
+		});
+	}
+}
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 5d671acf31..9f4975e888 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -122,6 +122,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</template>
 						</MkFolder>
 
+                        <div>
+							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="deleteUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.deleteUserAvatar }}</MkButton>
+							<MkButton v-if="iAmModerator" inline danger @click="deleteUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.deleteUserBanner }}</MkButton>
+						</div>
 						<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
 					</div>
 				</FormSection>
@@ -320,6 +324,44 @@ async function toggleSuspend(v) {
 	}
 }
 
+async function deleteUserAvatar() {
+  const confirm = await os.confirm({
+    type: 'warning',
+    text: i18n.ts.deleteUserAvatarConfirm,
+  });
+  if (confirm.canceled) return;
+  const process = async () => {
+    await os.api('admin/delete-user-avatar', { userId: user.id });
+    os.success();
+  };
+  await process().catch(err => {
+    os.alert({
+      type: 'error',
+      text: err.toString(),
+    });
+  });
+  refreshUser();
+}
+
+async function deleteUserBanner() {
+  const confirm = await os.confirm({
+    type: 'warning',
+    text: i18n.ts.deleteUserBannerConfirm,
+  });
+  if (confirm.canceled) return;
+  const process = async () => {
+    await os.api('admin/delete-user-banner', { userId: user.id });
+    os.success();
+  };
+  await process().catch(err => {
+    os.alert({
+      type: 'error',
+      text: err.toString(),
+    });
+  });
+  refreshUser();
+}
+
 async function deleteAllFiles() {
 	const confirm = await os.confirm({
 		type: 'warning',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 87922ba791..f4bcaa8066 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -345,6 +345,18 @@ export type Endpoints = {
         };
         res: null;
     };
+    'admin/delete-user-avatar': {
+        req: {
+            userId: User['id'];
+        };
+        res: null;
+    };
+    'admin/delete-user-banner': {
+        req: {
+            userId: User['id'];
+        };
+        res: null;
+    };
     'admin/delete-logs': {
         req: NoParams;
         res: null;
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 54b175fcf1..e3f28c644e 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -15,6 +15,8 @@ export type Endpoints = {
 	// admin
 	'admin/abuse-user-reports': { req: TODO; res: TODO; };
 	'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
+	'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; };
+	'admin/delete-user-banner': { req: { userId: User['id']; }; res: null; };
 	'admin/delete-logs': { req: NoParams; res: null; };
 	'admin/get-index-stats': { req: TODO; res: TODO; };
 	'admin/get-table-stats': { req: TODO; res: TODO; };

From 2f7d10bf2359823f5c37f8971bab287e7918a246 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 18 Nov 2023 21:08:32 +0900
Subject: [PATCH 06/10] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b08d12093..e2c226aec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
 
 ### General
 - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
+- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
 
 ### Client
 -

From b65fd349812fe3c89b5face6ec5c12823459d7df Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Nov 2023 10:18:57 +0900
Subject: [PATCH 07/10] tweak of 2b6f789a5b

---
 locales/en-US.yml                                |  8 ++++----
 locales/index.d.ts                               | 10 ++++++----
 locales/ja-JP.yml                                | 10 ++++++----
 .../backend/src/server/api/EndpointsModule.ts    | 16 ++++++++--------
 packages/backend/src/server/api/endpoints.ts     |  8 ++++----
 ...elete-user-avatar.ts => unset-user-avatar.ts} | 12 ++++++++++++
 ...elete-user-banner.ts => unset-user-banner.ts} | 12 ++++++++++++
 packages/backend/src/types.ts                    | 14 ++++++++++++++
 packages/frontend/src/pages/admin-user.vue       | 16 ++++++++--------
 packages/misskey-js/etc/misskey-js.api.md        |  4 ++--
 packages/misskey-js/src/api.types.ts             |  4 ++--
 packages/misskey-js/src/consts.ts                | 14 ++++++++++++++
 packages/misskey-js/src/entities.ts              |  6 ++++++
 13 files changed, 98 insertions(+), 36 deletions(-)
 rename packages/backend/src/server/api/endpoints/admin/{delete-user-avatar.ts => unset-user-avatar.ts} (77%)
 rename packages/backend/src/server/api/endpoints/admin/{delete-user-banner.ts => unset-user-banner.ts} (77%)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index 2aba028e45..b14592b20a 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -564,10 +564,10 @@ output: "Output"
 script: "Script"
 disablePagesScript: "Disable AiScript on Pages"
 updateRemoteUser: "Update remote user information"
-deleteUserAvatar: "Delete user icon"
-deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
-deleteUserBanner: "Delete user banner"
-deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
+unsetUserAvatar: "Delete user icon"
+unsetUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
+unsetUserBanner: "Delete user banner"
+unsetUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
 deleteAllFiles: "Delete all files"
 deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
 removeAllFollowing: "Unfollow all followed users"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 6fd6d3641a..39fbb57799 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -567,10 +567,10 @@ export interface Locale {
     "script": string;
     "disablePagesScript": string;
     "updateRemoteUser": string;
-    "deleteUserAvatar": string;
-    "deleteUserAvatarConfirm": string;
-    "deleteUserBanner": string;
-    "deleteUserBannerConfirm": string;
+    "unsetUserAvatar": string;
+    "unsetUserAvatarConfirm": string;
+    "unsetUserBanner": string;
+    "unsetUserBannerConfirm": string;
     "deleteAllFiles": string;
     "deleteAllFilesConfirm": string;
     "removeAllFollowing": string;
@@ -2417,6 +2417,8 @@ export interface Locale {
         "createAvatarDecoration": string;
         "updateAvatarDecoration": string;
         "deleteAvatarDecoration": string;
+        "unsetUserAvatar": string;
+        "unsetUserBanner": string;
     };
     "_fileViewer": {
         "title": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 9685e9c5a5..3757715c0f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -564,10 +564,10 @@ output: "出力"
 script: "スクリプト"
 disablePagesScript: "Pagesのスクリプトを無効にする"
 updateRemoteUser: "リモートユーザー情報の更新"
-deleteUserAvatar: "アイコンを削除"
-deleteUserAvatarConfirm: "アイコンを削除しますか?"
-deleteUserBanner: "バナーを削除"
-deleteUserBannerConfirm: "バナーを削除しますか?"
+unsetUserAvatar: "アイコンを解除"
+unsetUserAvatarConfirm: "アイコンを解除しますか?"
+unsetUserBanner: "バナーを解除"
+unsetUserBannerConfirm: "バナーを解除しますか?"
 deleteAllFiles: "すべてのファイルを削除"
 deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
 removeAllFollowing: "フォローを全解除"
@@ -2318,6 +2318,8 @@ _moderationLogTypes:
   createAvatarDecoration: "アイコンデコレーションを作成"
   updateAvatarDecoration: "アイコンデコレーションを更新"
   deleteAvatarDecoration: "アイコンデコレーションを削除"
+  unsetUserAvatar: "ユーザーのアイコンを解除"
+  unsetUserBanner: "ユーザーのバナーを解除"
 
 _fileViewer:
   title: "ファイルの詳細"
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 3797b46d04..86a64d7121 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -24,8 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
 import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
 import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
-import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
+import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
+import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -385,8 +385,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de
 const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
 const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
 const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
-const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default };
-const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default };
+const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
+const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
 const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
 const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
 const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
@@ -750,8 +750,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_avatarDecorations_list,
 		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
-		$admin_deleteUserAvatar,
-		$admin_deleteUserBanner,
+		$admin_unsetUserAvatar,
+		$admin_unsetUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
@@ -1109,8 +1109,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_avatarDecorations_list,
 		$admin_avatarDecorations_update,
 		$admin_deleteAllFilesOfAUser,
-		$admin_deleteUserAvatar,
-		$admin_deleteUserBanner,
+		$admin_unsetUserAvatar,
+		$admin_unsetUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4162ace337..e458d720ab 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -24,8 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
 import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
 import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
-import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
+import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
+import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -383,8 +383,8 @@ const eps = [
 	['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
 	['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
 	['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
-	['admin/delete-user-avatar', ep___admin_deleteUserAvatar],
-	['admin/delete-user-banner', ep___admin_deleteUserBanner],
+	['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
+	['admin/unset-user-banner', ep___admin_unsetUserBanner],
 	['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
 	['admin/drive/cleanup', ep___admin_drive_cleanup],
 	['admin/drive/files', ep___admin_drive_files],
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts
similarity index 77%
rename from packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
rename to packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts
index d3c78d7fb6..ac10f1b6fd 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unset-user-avatar.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import type { UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -29,6 +30,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 	constructor(
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
+
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -36,6 +39,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			if (user == null) {
 				throw new Error('user not found');
 			}
+	
+			if (user.avatarId == null) return;
 
 			await this.usersRepository.update(user.id, {
 				avatar: null,
@@ -43,6 +48,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				avatarUrl: null,
 				avatarBlurhash: null,
 			});
+
+			this.moderationLogService.log(me, 'unsetUserAvatar', {
+				userId: user.id,
+				userUsername: user.username,
+				userHost: user.host,
+				fileId: user.avatarId,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts
similarity index 77%
rename from packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
rename to packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts
index e076cdcfc1..66acd367df 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unset-user-banner.ts
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import type { UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -29,6 +30,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 	constructor(
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
+
+		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const user = await this.usersRepository.findOneBy({ id: ps.userId });
@@ -37,12 +40,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw new Error('user not found');
 			}
 
+			if (user.bannerId == null) return;
+
 			await this.usersRepository.update(user.id, {
 				banner: null,
 				bannerId: null,
 				bannerUrl: null,
 				bannerBlurhash: null,
 			});
+
+			this.moderationLogService.log(me, 'unsetUserBanner', {
+				userId: user.id,
+				userUsername: user.username,
+				userHost: user.host,
+				fileId: user.bannerId,
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index e6dfeb6f8c..1fb3d6a6ce 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -63,6 +63,8 @@ export const moderationLogTypes = [
 	'createAvatarDecoration',
 	'updateAvatarDecoration',
 	'deleteAvatarDecoration',
+	'unsetUserAvatar',
+	'unsetUserBanner',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -237,6 +239,18 @@ export type ModerationLogPayloads = {
 		avatarDecorationId: string;
 		avatarDecoration: any;
 	};
+	unsetUserAvatar: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+		fileId: string;
+	};
+	unsetUserBanner: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+		fileId: string;
+	};
 };
 
 export type Serialized<T> = {
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 9f4975e888..87ebedc296 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -123,8 +123,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkFolder>
 
                         <div>
-							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="deleteUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.deleteUserAvatar }}</MkButton>
-							<MkButton v-if="iAmModerator" inline danger @click="deleteUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.deleteUserBanner }}</MkButton>
+							<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
+							<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
 						</div>
 						<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
 					</div>
@@ -324,14 +324,14 @@ async function toggleSuspend(v) {
 	}
 }
 
-async function deleteUserAvatar() {
+async function unsetUserAvatar() {
   const confirm = await os.confirm({
     type: 'warning',
-    text: i18n.ts.deleteUserAvatarConfirm,
+    text: i18n.ts.unsetUserAvatarConfirm,
   });
   if (confirm.canceled) return;
   const process = async () => {
-    await os.api('admin/delete-user-avatar', { userId: user.id });
+    await os.api('admin/unset-user-avatar', { userId: user.id });
     os.success();
   };
   await process().catch(err => {
@@ -343,14 +343,14 @@ async function deleteUserAvatar() {
   refreshUser();
 }
 
-async function deleteUserBanner() {
+async function unsetUserBanner() {
   const confirm = await os.confirm({
     type: 'warning',
-    text: i18n.ts.deleteUserBannerConfirm,
+    text: i18n.ts.unsetUserBannerConfirm,
   });
   if (confirm.canceled) return;
   const process = async () => {
-    await os.api('admin/delete-user-banner', { userId: user.id });
+    await os.api('admin/unset-user-banner', { userId: user.id });
     os.success();
   };
   await process().catch(err => {
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index f4bcaa8066..85907de665 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -345,13 +345,13 @@ export type Endpoints = {
         };
         res: null;
     };
-    'admin/delete-user-avatar': {
+    'admin/unset-user-avatar': {
         req: {
             userId: User['id'];
         };
         res: null;
     };
-    'admin/delete-user-banner': {
+    'admin/unset-user-banner': {
         req: {
             userId: User['id'];
         };
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index e3f28c644e..1a75b7cf57 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -15,8 +15,8 @@ export type Endpoints = {
 	// admin
 	'admin/abuse-user-reports': { req: TODO; res: TODO; };
 	'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
-	'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; };
-	'admin/delete-user-banner': { req: { userId: User['id']; }; res: null; };
+	'admin/unset-user-avatar': { req: { userId: User['id']; }; res: null; };
+	'admin/unset-user-banner': { req: { userId: User['id']; }; res: null; };
 	'admin/delete-logs': { req: NoParams; res: null; };
 	'admin/get-index-stats': { req: TODO; res: TODO; };
 	'admin/get-table-stats': { req: TODO; res: TODO; };
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index 48a36a31d6..a8f0b96d5d 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -81,6 +81,8 @@ export const moderationLogTypes = [
 	'createAvatarDecoration',
 	'updateAvatarDecoration',
 	'deleteAvatarDecoration',
+	'unsetUserAvatar',
+	'unsetUserBanner',
 ] as const;
 
 export type ModerationLogPayloads = {
@@ -255,4 +257,16 @@ export type ModerationLogPayloads = {
 		avatarDecorationId: string;
 		avatarDecoration: any;
 	};
+	unsetUserAvatar: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+		fileId: string;
+	};
+	unsetUserBanner: {
+		userId: string;
+		userUsername: string;
+		userHost: string | null;
+		fileId: string;
+	};
 };
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index a0d0b7528d..a51315b13b 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -727,4 +727,10 @@ export type ModerationLog = {
 } | {
 	type: 'resolveAbuseReport';
 	info: ModerationLogPayloads['resolveAbuseReport'];
+} | {
+	type: 'unsetUserAvatar';
+	info: ModerationLogPayloads['unsetUserAvatar'];
+} | {
+	type: 'unsetUserBanner';
+	info: ModerationLogPayloads['unsetUserBanner'];
 });

From cbebe85ccfab582f9cdf6f3680673e80d708439c Mon Sep 17 00:00:00 2001
From: Lynx Kotoura <admin@sanin.link>
Date: Sun, 19 Nov 2023 11:43:04 +0900
Subject: [PATCH 08/10] =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E4=B8=80?=
 =?UTF-8?q?=E8=A6=A7=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=8C=E3=83=A2=E3=83=90=E3=82=A4=E3=83=AB=E7=92=B0=E5=A2=83?=
 =?UTF-8?q?=E3=81=AB=E3=81=8A=E3=81=84=E3=81=A6=E5=B4=A9=E3=82=8C=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1235?=
 =?UTF-8?q?4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix style of list of pages on mobile

* overflow clip に変えた
---
 CHANGELOG.md                                       | 2 +-
 packages/frontend/src/components/MkPagePreview.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2c226aec0..d96af455f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
 -
 
 ### Client
--
+- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
 
 ### Server
 -
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 05b577c49c..6c8a0e56a6 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -114,7 +114,6 @@ const props = defineProps<{
 
 			& + article {
 				left: 0;
-				width: 100%;
 			}
 		}
 	}
@@ -124,6 +123,7 @@ const props = defineProps<{
 
 		> .thumbnail {
 			height: 80px;
+			overflow: clip;
 		}
 
 		> article {

From 02b0adf31facbe7eb6d5905e9670ee541e2585e6 Mon Sep 17 00:00:00 2001
From: yukineko <27853966+hideki0403@users.noreply.github.com>
Date: Sun, 19 Nov 2023 11:45:24 +0900
Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=E3=80=8C=E8=A8=AD=E5=AE=9A?=
 =?UTF-8?q?=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF=E3=82=A2=E3=83=83=E3=83=97?=
 =?UTF-8?q?=E3=80=8D=E3=81=AB=E4=B8=80=E9=83=A8=E3=81=AE=E8=A8=AD=E5=AE=9A?=
 =?UTF-8?q?=E9=A0=85=E7=9B=AE=E3=81=8C=E5=90=AB=E3=81=BE=E3=82=8C=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3=20(#12366)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: 一部の設定項目がバックアップに含まれていなかったのを修正

* update: CHANGELOG.md

* remove: バックアップ不要な項目を削除
---
 CHANGELOG.md                                  |  2 +-
 .../pages/settings/preferences-backups.vue    | 25 +++++++++++++++++--
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d96af455f1..0bdbb2aade 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@
 - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
 
 ### Client
--
+- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
 
 ### Server
 -
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 3b3a6bd07d..35435238fc 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -54,22 +54,24 @@ import { miLocalStorage } from '@/local-storage.js';
 const { t, ts } = i18n;
 
 const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
+	'collapseRenotes',
 	'menu',
 	'visibility',
 	'localOnly',
 	'statusbars',
 	'widgets',
 	'tl',
+	'pinnedUserLists',
 	'overridedDeviceKind',
 	'serverDisconnectedBehavior',
-	'collapseRenotes',
-	'showNoteActionsOnlyHover',
 	'nsfw',
+	'highlightSensitiveMedia',
 	'animation',
 	'animatedMfm',
 	'advancedMfm',
 	'loadRawImages',
 	'imageNewTab',
+	'enableDataSaverMode',
 	'disableShowingAnimatedImages',
 	'emojiStyle',
 	'disableDrawer',
@@ -89,9 +91,28 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'menuDisplay',
 	'reportError',
 	'squareAvatars',
+	'showAvatarDecorations',
 	'numberOfPageCache',
+	'showNoteActionsOnlyHover',
+	'showClipButtonInNoteFooter',
+	'reactionsDisplaySize',
+	'forceShowAds',
 	'aiChanMode',
+	'devMode',
 	'mediaListWithOneImageAppearance',
+	'notificationPosition',
+	'notificationStackAxis',
+	'enableCondensedLineForAcct',
+	'keepScreenOn',
+	'defaultWithReplies',
+	'disableStreamingTimeline',
+	'useGroupedNotifications',
+	'sound_masterVolume',
+	'sound_note',
+	'sound_noteMy',
+	'sound_notification',
+	'sound_antenna',
+	'sound_channel',
 ];
 const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
 	'lightTheme',

From e0de86359c9df510e02a2fa25643030d0517afdf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9E=9C=E7=89=A9=E3=83=AA=E3=83=B3?= <nassii74@gmail.com>
Date: Sun, 19 Nov 2023 13:39:25 +0900
Subject: [PATCH 10/10] =?UTF-8?q?backend=E3=81=AE=E3=83=97=E3=83=AD?=
 =?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=A7=E5=8D=98=E4=BD=93?=
 =?UTF-8?q?=E3=81=A7=20start=20=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?=
 =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#12371)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index a4856709c3..496c79c9c0 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -7,8 +7,8 @@
 		"node": ">=18.16.0"
 	},
 	"scripts": {
-		"start": "node ./built/index.js",
-		"start:test": "NODE_ENV=test node ./built/index.js",
+		"start": "node ./built/boot/entry.js",
+		"start:test": "NODE_ENV=test node ./built/boot/entry.js",
 		"migrate": "pnpm typeorm migration:run -d ormconfig.js",
 		"revert": "pnpm typeorm migration:revert -d ormconfig.js",
 		"check:connect": "node ./check_connect.js",